Static Members
Static members belong to the class itself, not to any individual instance. They are accessed via the class name and exist only once in memory — no matter how many instances you create.
This makes them ideal for shared configuration, factory methods, caches, counters, and utility helpers that logically belong to a class but don't need access to instance state.
Static properties
class Counter {
static count = 0; // shared across all instances
constructor(public name: string) {
Counter.count++; // increment the shared counter
}
}
new Counter('Alice');
new Counter('Bob');
new Counter('Carol');
console.log(Counter.count); // 3 — not accessed via an instanceCounter.count), not with this.count or an instance variable. this inside a static method refers to the class constructor, not an instance.Static methods
Static methods are class-level utilities. They cannot access instance properties (no this.someField) but they can access other static members via this (which refers to the class constructor when called statically).
class MathUtils {
static PI = 3.14159265358979;
static circleArea(radius: number): number {
return MathUtils.PI * radius ** 2;
}
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
static lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
}
console.log(MathUtils.circleArea(5)); // 78.539...
console.log(MathUtils.clamp(150, 0, 100)); // 100
console.log(MathUtils.lerp(0, 10, 0.5)); // 5Factory pattern with static methods
One of the most useful patterns for static methods: named constructors (factory methods). They give descriptive names to different ways of creating an instance and can enforce invariants before construction.
class Color {
private constructor(
public readonly r: number,
public readonly g: number,
public readonly b: number,
) {}
// Factory: create from 0–255 integers
static fromRGB(r: number, g: number, b: number): Color {
if ([r, g, b].some(v => v < 0 || v > 255)) {
throw new RangeError('RGB values must be 0–255');
}
return new Color(r, g, b);
}
// Factory: create from a hex string like '#ff8800'
static fromHex(hex: string): Color {
const clean = hex.replace('#', '');
const r = parseInt(clean.slice(0, 2), 16);
const g = parseInt(clean.slice(2, 4), 16);
const b = parseInt(clean.slice(4, 6), 16);
return new Color(r, g, b);
}
// Predefined constants
static readonly RED = new Color(255, 0, 0);
static readonly GREEN = new Color(0, 255, 0);
static readonly BLUE = new Color(0, 0, 255);
toHex(): string {
return '#' + [this.r, this.g, this.b]
.map(v => v.toString(16).padStart(2, '0'))
.join('');
}
}
const orange = Color.fromHex('#ff8800');
console.log(orange.toHex()); // #ff8800
console.log(Color.RED.toHex()); // #ff0000constructor private forces callers to use the factory methods — they cannot do new Color(...) accidentally. This is the Factory Method pattern.Singleton pattern
A singleton guarantees at most one instance of a class ever exists. Static members make this straightforward:
class AppConfig {
private static _instance: AppConfig | null = null;
private readonly _settings: Map<string, string> = new Map();
private constructor() {
// Load defaults
this._settings.set('theme', 'light');
this._settings.set('language', 'en');
}
static getInstance(): AppConfig {
if (!AppConfig._instance) {
AppConfig._instance = new AppConfig();
}
return AppConfig._instance;
}
get(key: string): string | undefined {
return this._settings.get(key);
}
set(key: string, value: string): void {
this._settings.set(key, value);
}
}
const cfg1 = AppConfig.getInstance();
const cfg2 = AppConfig.getInstance();
cfg1.set('theme', 'dark');
console.log(cfg2.get('theme')); // 'dark' — same object
console.log(cfg1 === cfg2); // trueStatic blocks (ES2022 / TypeScript 4.4+)
Static initialization blocks run once when the class is first evaluated. They are useful when static field initialization requires complex logic, try/catch, or depends on other static fields.
class DatabasePool {
static readonly maxConnections: number;
static readonly connectionString: string;
static {
// Complex initialization — read from environment, validate, transform
const raw = process.env['DB_MAX_CONNECTIONS'] ?? '10';
const parsed = parseInt(raw, 10);
if (isNaN(parsed) || parsed < 1) {
throw new Error(`Invalid DB_MAX_CONNECTIONS: "${raw}"`);
}
DatabasePool.maxConnections = parsed;
DatabasePool.connectionString = process.env['DATABASE_URL'] ?? 'sqlite::memory:';
console.log(`Pool initialized: ${DatabasePool.maxConnections} connections`);
}
}Static members and inheritance
Static members are inherited through the prototype chain of the class constructors. A subclass can override static members and access the parent's via super.
class Animal {
static category = 'Unknown';
static describe(): string {
// 'this' in a static method is the class (constructor)
return `Category: ${this.category}`;
}
}
class Dog extends Animal {
static override category = 'Mammal';
static override describe(): string {
return `${super.describe()} (Dog)`;
}
}
class Salmon extends Animal {
static override category = 'Fish';
}
console.log(Animal.describe()); // Category: Unknown
console.log(Dog.describe()); // Category: Mammal (Dog)
console.log(Salmon.describe()); // Category: Fishthis.category (not Animal.category) in the static method is the key — it makes the method polymorphic and respects overrides in subclasses.Type of static members
TypeScript gives you the typeof ClassName type to represent the class constructor itself (rather than an instance). This is useful when passing classes as values.
class Logger {
static prefix = '[LOG]';
static log(msg: string): void {
console.log(`${Logger.prefix} ${msg}`);
}
}
// typeof Logger is the constructor type
function useLogger(LoggerClass: typeof Logger): void {
LoggerClass.log('Hello');
}
useLogger(Logger); // [LOG] Hello
// Useful pattern: abstract factory via static methods
class ServiceFactory {
static create<T>(Cls: new () => T): T {
return new Cls();
}
}
class MyService { greet() { return 'hi'; } }
const svc = ServiceFactory.create(MyService);
console.log(svc.greet()); // hiReal-world example: registry pattern
type Handler = (payload: unknown) => void;
class EventBus {
private static handlers: Map<string, Set<Handler>> = new Map();
static on(event: string, handler: Handler): void {
if (!EventBus.handlers.has(event)) {
EventBus.handlers.set(event, new Set());
}
EventBus.handlers.get(event)!.add(handler);
}
static off(event: string, handler: Handler): void {
EventBus.handlers.get(event)?.delete(handler);
}
static emit(event: string, payload: unknown): void {
EventBus.handlers.get(event)?.forEach(h => h(payload));
}
}
EventBus.on('login', (user) => console.log('User logged in:', user));
EventBus.on('login', (user) => console.log('Audit:', user));
EventBus.emit('login', { id: 1, name: 'Alice' });
// User logged in: { id: 1, name: 'Alice' }
// Audit: { id: 1, name: 'Alice' }When to use static members
Use case | Static? | Notes |
|---|---|---|
Shared counter / registry | ✅ | Single copy across all instances |
Utility / helper methods (no instance state) | ✅ | No need to instantiate |
Factory / named constructors | ✅ | Controls object creation |
Singleton instance | ✅ | Store instance as static field |
Configuration constants | ✅ | static readonly |
Per-instance state | ❌ | Use regular instance fields |
Methods that use instance data | ❌ | Must be instance methods |
Pitfalls to avoid
Overusing static: if a "utility class" is just a namespace of static methods, consider a plain module with exported functions instead.
Mutable global state via static fields is hard to test — each test pollutes the next. Prefer dependency injection.
Accessing a static member via
thisin an instance method works at runtime but TypeScript may warn — use the class name explicitly for clarity.Static fields are not automatically reset between tests. Use
beforeEachto reset shared state in test suites.