npm package: @ludeschersoftware/singleton
Description: A enforcable Singleton class
Author: Johannes Ludescher
Latest version: 1.0.4

A lightweight, **enforceable Singleton base class for TypeScript**, designed for correctness, testability, and strong typing.

This package provides a clean alternative to traditional singleton implementations by **disabling constructors**, **enforcing a single instance per subclass**, and exposing an explicit lifecycle method for initialization.

It combines strict runtime guarantees with a minimal, framework-agnostic API.

---

## ✨ Key Concepts

* ❌ **No constructors** – direct instantiation is impossible
* ✅ **Exactly one instance per subclass**
* 🧠 **Fully type-safe** `getInstance()` API
* 💤 **Lazy initialization** (created on first access)
* 🔁 **Explicit lifecycle hook** instead of constructors
* 🧪 **Resettable instances** for deterministic tests
* 🪶 Zero dependencies

---

## 📦 Installation

```bash
npm install @ludeschersoftware/singleton
# or
yarn add @ludeschersoftware/singleton
```

---

## 🚀 Basic Usage

Extend the `Singleton` base class and implement the `initialize()` lifecycle method.

```ts
import { Singleton } from '@ludeschersoftware/singleton';

class ConfigService extends Singleton {
  public config!: Record<string, string>;

  protected initialize(): void {
    this.config = {
      env: 'production',
    };
  }
}

const a = ConfigService.getInstance();
const b = ConfigService.getInstance();

console.log(a === b); // true
console.log(a.config.env); // 'production'
```

Attempting to instantiate a singleton directly will fail:

```ts
new ConfigService();
// ❌ Error: A Singleton does not have a constructor!
```

---

## 🔁 Initialization with Parameters

`getInstance()` supports parameters that are forwarded to `initialize()` **exactly once**, when the instance is first created.

```ts
class Database extends Singleton {
  public url!: string;

  protected initialize(url: string): void {
    this.url = url;
  }
}

const db = Database.getInstance('postgres://localhost');

// Subsequent calls ignore parameters and return the same instance
Database.getInstance('ignored');
```

> ⚠️ Parameters are only respected on the **first call** to `getInstance()`.

---

## 🛠️ API Reference

### `abstract class Singleton`

Base class for all singletons.

---

### `static getInstance<T>(...params): T`

Returns the singleton instance of the subclass.

* Creates the instance lazily
* Guarantees exactly one instance per subclass
* Calls `initialize(...params)` exactly once
* Fully type-safe for subclasses

```ts
const instance = MySingleton.getInstance(...args);
```

---

### `protected initialize(...params): void`

Lifecycle hook invoked once when the singleton is first created.

* Replaces constructor logic
* Receives parameters from `getInstance()`
* Must be implemented by subclasses

```ts
protected initialize(config: Config): void {
  // setup logic
}
```

---

### `static _resetInstance(): void`

Deletes the current singleton instance.

* Intended **only for testing**
* Allows deterministic test isolation
* Safe to call multiple times

```ts
MySingleton._resetInstance();
```

---

## 🧪 Testing Example

```ts
class TestService extends Singleton {
  public initialized = false;

  protected initialize(): void {
    this.initialized = true;
  }
}

describe('TestService', () => {
  afterEach(() => {
    TestService._resetInstance();
  });

  it('creates a fresh instance per test', () => {
    const instance = TestService.getInstance();
    expect(instance.initialized).toBe(true);
  });
});
```

---

## 🧩 Common Use Cases

* Application-wide configuration
* Logging and telemetry services
* Connection pools
* Caches and registries
* Cross-module shared state (without globals)

---

## ⚙️ Design Notes

* Instances are stored internally using a `Symbol`, preventing accidental overrides
* No reliance on global variables
* Runtime enforcement ensures misuse fails fast
* Compatible with ESM and CommonJS builds

---

## 📄 License

MIT © Johannes Ludescher

---

## 💬 Feedback & Contributions

Issues, ideas, and pull requests are welcome. If you have suggestions or edge cases to discuss, feel free to open an issue.