TypeScriptStatic Members

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

TS
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 instance
Note
Access static members with the class name (Counter.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).

TS
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));    // 5
Factory 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.

TS
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()); // #ff0000
Tip
Making the constructor 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:

TS
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);     // true
Static 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.

TS
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.

TS
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: Fish
Note
Using this.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.

TS
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()); // hi
Real-world example: registry pattern

TS
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 this in 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 beforeEach to reset shared state in test suites.

Success
Static members are the TypeScript way to express class-level state and behaviour — counters, factories, caches, and singletons. Used with discipline they reduce coupling and centralize shared logic cleanly.