Skip to main content

TS Ep 23: Static Members, Static Blocks, and Singletons

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 23: This Article
Standard class members belong to individual instances created via new. In contrast, Static Members belong to the Class Constructor Function itself. They allow defining shared utility methods, global constants, and singletons.

1. Instance Members vs Static Members
#

To understand static members, visualize where properties reside in memory:

  • Instance Members: Copied or linked to every object created via new MyClass().
  • Static Members: Stored once on the MyClass constructor object itself.
class MathOperations {
  // Static Constant
  public static readonly PI: number = 3.14159265359;

  // Static Utility Method
  public static calculateCircleArea(radius: number): number {
    return MathOperations.PI * (radius ** 2);
  }
}

// 🟢 WORKED: Accessing static members directly on the class constructor!
console.log(MathOperations.PI); // 3.14159265359
console.log(MathOperations.calculateCircleArea(10)); // ~314.159

// ❌ Compiler Error: Property 'PI' does not exist on type 'MathOperations' (instance).
// const inst = new MathOperations();
// console.log(inst.PI);

2. Static Access Modifiers (public static, private static)
#

Just like instance members, static members can be combined with public, private, and protected access modifiers:

class ApplicationConfig {
  // Public static constant
  public static readonly APP_NAME = "E-Commerce Core";

  // Private static storage
  private static activeConnections: number = 0;

  // Private static helper method
  private static logConnectionChange() {
    console.log(`[CONFIG]: Active connections updated to ${ApplicationConfig.activeConnections}`);
  }

  public static trackNewConnection() {
    ApplicationConfig.activeConnections++;
    ApplicationConfig.logConnectionChange();
  }

  public static getActiveConnections(): number {
    return ApplicationConfig.activeConnections;
  }
}

ApplicationConfig.trackNewConnection(); // "[CONFIG]: Active connections updated to 1"

// ❌ Compiler Error: Property 'activeConnections' is private and only accessible within class 'ApplicationConfig'.
// console.log(ApplicationConfig.activeConnections);

3. Static Initialization Blocks (static {})
#

Introduced in ECMAScript 2022 (and TypeScript 4.4), Static Blocks allow you to write complex, multi-statement initialization logic for static fields, complete with try/catch error handling and private field access.

class EnvironmentRegistry {
  public static readonly API_ENDPOINT: string;
  public static readonly IS_PRODUCTION: boolean;

  // Static Initialization Block (Runs ONCE when the class is loaded into memory)
  static {
    try {
      const rawEnv = process.env.NODE_ENV || "development";
      EnvironmentRegistry.IS_PRODUCTION = rawEnv === "production";

      if (EnvironmentRegistry.IS_PRODUCTION) {
        EnvironmentRegistry.API_ENDPOINT = "https://api.acme.com/v1";
      } else {
        EnvironmentRegistry.API_ENDPOINT = "http://localhost:8080/v1";
      }

      console.log(`[Registry]: Environment initialized as ${rawEnv}`);
    } catch (err) {
      EnvironmentRegistry.API_ENDPOINT = "http://fallback.local";
      EnvironmentRegistry.IS_PRODUCTION = false;
    }
  }
}

console.log(EnvironmentRegistry.API_ENDPOINT);

Why use static {} blocks?
#

Static blocks can access private static fields from within the class body, allowing complex setup logic that standard inline property initializers cannot perform.


4. The Singleton Design Pattern
#

The Singleton Pattern restricts the instantiation of a class to one single, shared instance across the entire application runtime. It is frequently used for database pools, logger services, and state stores.

How to Build a Singleton in TypeScript:
#

  1. Make the constructor() private so external callers cannot invoke new DatabaseConnection().
  2. Store the single instance in a private static variable.
  3. Provide a public static getInstance() method to lazily construct and return the instance.
class DatabasePoolManager {
  // 1. Private static variable holding the single instance
  private static instance: DatabasePoolManager | null = null;

  private connectionId: string;

  // 2. PRIVATE CONSTRUCTOR stops external callers from using 'new'!
  private constructor() {
    this.connectionId = `conn_${Math.random().toString(36).substring(2, 9)}`;
    console.log(`[DatabasePool]: Opened pool with ID: ${this.connectionId}`);
  }

  // 3. Public static getter for lazy initialization
  public static getInstance(): DatabasePoolManager {
    if (DatabasePoolManager.instance === null) {
      DatabasePoolManager.instance = new DatabasePoolManager();
    }
    return DatabasePoolManager.instance;
  }

  public query(sql: string) {
    console.log(`Executing SQL on pool ${this.connectionId}: ${sql}`);
  }
}

// ❌ Compiler Error: Constructor of class 'DatabasePoolManager' is private and only accessible within the class declaration.
// const badDb = new DatabasePoolManager();

// 🟢 WORKED: Retrieving the singleton instance
const db1 = DatabasePoolManager.getInstance(); // "[DatabasePool]: Opened pool with ID: conn_a1b2c3"
const db2 = DatabasePoolManager.getInstance(); // (No output! Returns existing instance)

db1.query("SELECT * FROM users");

// Both variables point to the exact same reference in memory!
console.log(db1 === db2); // true!

5. Reserved Static Names in JavaScript
#

Because static members attach directly to the Class Constructor Function (which is an instance of JavaScript’s built-in Function object), certain property names are forbidden as static declarations:

  • name (Function name property)
  • length (Function arity property)
  • caller
  • arguments
  • prototype
class CustomWidget {
  // ❌ Compiler Error: Static property 'name' conflicts with built-in property 'Function.name'.
  // public static name: string = "Widget";

  // 🟢 GOOD: Use a non-reserved identifier name
  public static widgetName: string = "Widget";
}

Summary & Next Steps
#

In this episode:

  • We distinguished instance members (new Class()) from static members (Class.member).
  • We combined static members with access modifiers (private static, public static).
  • We executed complex startup logic using ES2022 Static Initialization Blocks (static {}).
  • We implemented the Singleton Pattern using private constructors and static getInstance() methods.
  • We identified reserved static names (name, length, prototype).

In Episode 24: The Polymorphic this Type, we will explore fluent builder APIs and method chaining using TypeScript’s polymorphic this return type!

typescript - This article is part of a series.
Part 23: This Article