@ludeschersoftware/singleton
v1.0.4
Published
A enforcable Singleton class
Readme
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
npm install @ludeschersoftware/singleton
# or
yarn add @ludeschersoftware/singleton🚀 Basic Usage
Extend the Singleton base class and implement the initialize() lifecycle method.
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:
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.
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
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
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
MySingleton._resetInstance();🧪 Testing Example
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.
