TypeScript Static Members

A static member belongs to the class itself, not to any particular instance created from it. Where a regular (instance) property or method exists separately on every object you build with new, a static member exists exactly once, attached to the class. This makes static members the natural place for shared counters, configuration constants, factory methods, and utility functions that logically belong to a class but don’t need an object to operate on.

Overview / How it works

Every class in TypeScript has two “sides” as far as the type checker is concerned: the instance side (the shape of objects produced by new ClassName()) and the static side (the shape of the class constructor object itself, sometimes written as the type typeof ClassName). Instance members — properties and methods declared without the static keyword — live on the instance side and are accessed through an object reference (myObject.prop). Static members live on the static side and are accessed through the class name directly (ClassName.prop), never through an instance.

Unlike most of TypeScript’s type-level features, static is not erased at compile time — it is a real JavaScript concept. When TypeScript compiles a class, static properties and methods become properties directly on the compiled constructor function (or class object), exactly the way JavaScript’s native class syntax already works. What does get erased is everything type-only: the type annotations on a static property, the visibility keyword private (which is a compile-time-only check — the property is still a normal enumerable field at runtime), and any generic parameters. Genuine runtime privacy for static members requires the # private field syntax, which the JavaScript engine itself enforces, not just the compiler.

Static members are inherited by subclasses. A subclass can read, override, or add to the static members of its parent, and it can refer to them either through its own name or the parent’s name. Inside a static method, the keyword this refers to the class itself (its static side), and — importantly — when a subclass calls an inherited static method, this refers to the subclass, not the class where the method was originally written. This enables polymorphic factory patterns.

Syntax

class ClassName {
  static staticProperty: Type = initialValue;
  static readonly staticReadonly: Type = initialValue;
  #privateInstance: Type;
  static #staticPrivate: Type;

  static staticMethod(): ReturnType {
    // static methods can access other static members via ClassName.member
    return staticInitialValueOrComputed;
  }

  static {
    // static initialization block, runs once when the class is defined
  }
}
  • static staticProperty — a field attached to the class, one copy shared by everyone.
  • static readonly — combines static with readonly for a class-wide constant that cannot be reassigned after its initializer runs.
  • static #staticPrivate — a static field with true runtime privacy; only code inside the class body can reference it.
  • static methodName() — a method invoked as ClassName.methodName(), never through an instance.
  • static { ... } — a static initialization block (available from TypeScript 4.7+), used for setup logic too complex for a single field initializer expression.

Examples

The first example uses a static property to count how many instances of a class have been created — a classic use case, since the count is a fact about the class as a whole, not about any single widget.

class Widget {
  static count: number = 0;
  id: number;

  constructor() {
    Widget.count++;
    this.id = Widget.count;
  }

  static getCount(): number {
    return Widget.count;
  }
}

const w1 = new Widget();
const w2 = new Widget();
const w3 = new Widget();

console.log(Widget.getCount());
console.log(w1.id, w2.id, w3.id);

Output:

3
1 2 3

Each call to new Widget() bumps the shared Widget.count and stamps the new instance with an id. Notice that count is read and written entirely through Widget., never through this.count inside an instance method context that would be ambiguous — the constructor explicitly writes Widget.count++ so there’s no confusion about which “side” is being mutated.

The second example combines a static readonly constant with a private static field to implement the singleton pattern, where a class guarantees only one instance ever exists.

class AppConfig {
  static readonly VERSION: string = "1.4.0";
  private static instance: AppConfig | undefined;

  private constructor(public readonly apiUrl: string) {}

  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig("https://api.example.com");
    }
    return AppConfig.instance;
  }
}

const configA = AppConfig.getInstance();
const configB = AppConfig.getInstance();

console.log(AppConfig.VERSION);
console.log(configA === configB);
console.log(configA.apiUrl);

Output:

1.4.0
true
https://api.example.com

The constructor is marked private, so the only way to obtain an AppConfig from outside the class is through the static getInstance() factory method. The static instance field caches the single object that gets created, so configA and configB are literally the same object (=== is true). VERSION is a static constant that has nothing to do with any particular config object, so it makes sense as static readonly rather than an instance field.

The third example shows a static initialization block, used when setting up static state requires more than one line of logic, plus true private static fields with #.

class Database {
  static #connectionString: string;
  static #isReady: boolean = false;

  static {
    const host = "localhost";
    const port = 5432;
    Database.#connectionString = `postgres://${host}:${port}/app`;
    Database.#isReady = true;
  }

  static getConnectionString(): string {
    return Database.#connectionString;
  }

  static isReady(): boolean {
    return Database.#isReady;
  }
}

console.log(Database.getConnectionString());
console.log(Database.isReady());

Output:

postgres://localhost:5432/app
true

The static { ... } block runs exactly once, at the moment the class itself is defined — before any instance of Database is ever created. It can contain arbitrary statements (local variables, loops, conditionals), which is more flexible than a single field initializer expression. The fields #connectionString and #isReady use JavaScript’s true private field syntax, so no code outside the class — not even a subclass — can read or write them directly; they can only be reached through the public static methods.

How it works step by step / Under the hood

When the TypeScript compiler processes a class, it separates members into two groups based on the presence of the static keyword, and type-checks references to them accordingly: an expression like instance.staticMember is rejected, and so is ClassName.instanceMember, because the checker knows which side of the class each member lives on.

At the JavaScript level (after type annotations are stripped), static property initializers and static blocks run in the order they’re written, top to bottom, interleaved with each other, all at the moment the class declaration itself is evaluated — this happens once, regardless of how many instances you later create with new. Static methods become ordinary properties on the constructor function/class object, which is why this inside a static method resolves to that constructor object (or a subclass’s constructor object, if called through a subclass) rather than to any instance.

Because the compiled output has no type information at all, everything that survives is exactly the runtime shape you’d get from writing native JavaScript classes: static fields as properties on the class object, static private # fields enforced by the engine, and static methods as callable properties on that same object. The only things type annotations, private/public/protected keywords, and generic parameters contribute are compile-time checks — they vanish entirely from the emitted .js.

Common Mistakes

Mistake 1: Accessing a static member through an instance. It’s tempting to reach for this.staticProp or instance.staticProp, but the compiler rejects it because static members simply aren’t part of the instance’s type.

class Counter {
  static total: number = 0;
  count: number = 0;

  increment(): void {
    this.count++;
    Counter.total++;
  }
}

const c = new Counter();
c.increment();
console.log(c.total);

tsc reports: Property 'total' does not exist on type 'Counter'. Did you mean to access the static member 'Counter.total' instead? The fix is to always reference the static member through the class name:

class Counter {
  static total: number = 0;
  count: number = 0;

  increment(): void {
    this.count++;
    Counter.total++;
  }
}

const c = new Counter();
c.increment();
console.log(Counter.total);

Output:

1

Mistake 2: Referencing an instance property from inside a static method. Since this in a static method refers to the class, not any object, it has no access to instance fields like name.

class Employee {
  name: string;
  static count: number = 0;

  constructor(name: string) {
    this.name = name;
    Employee.count++;
  }

  static printLastName(): void {
    console.log(this.name);
  }
}

tsc reports: Property 'name' does not exist on type 'typeof Employee'. The fix is to pass the instance you want to operate on as a parameter, instead of pretending a static method can reach into “the” instance:

class Employee {
  name: string;
  static count: number = 0;

  constructor(name: string) {
    this.name = name;
    Employee.count++;
  }

  static printName(employee: Employee): void {
    console.log(employee.name);
  }
}

const e = new Employee("Ada");
Employee.printName(e);
console.log(Employee.count);

Output:

Ada
1

Best Practices

  • Use static only for things that are genuinely properties of the class as a whole — counters, caches, configuration constants, and factory methods — not as a dumping ground for unrelated helper functions.
  • Prefer static readonly for class-wide constants so the compiler catches accidental reassignment.
  • Use true private # static fields (not just the private keyword) when static state must be genuinely inaccessible from outside the class, such as a singleton’s cached instance.
  • Reach for a static block when initializing static state requires more than a single expression — loops, conditionals, or multiple intermediate variables.
  • Be cautious with mutable static state in code you plan to unit test; shared static counters can leak between test cases unless explicitly reset.
  • Remember static members are inherited — when designing base classes with static factory methods, decide deliberately whether this inside them should resolve polymorphically to subclasses.
  • Never try to access a static member through this inside an instance method or through an object reference — always use the class name.

Practice Exercises

1. Write an IdGenerator class with a private static counter and a public static method next() that returns a new incrementing number starting at 1 each time it’s called.

2. Write a Circle class with a static readonly PI constant and a static method area(radius: number): number that computes the area of a circle without requiring a Circle instance.

3. Take your IdGenerator or Circle class and add a static block that logs a one-time setup message (for example, "IdGenerator ready") the moment the class is loaded, before any method is called.

Summary

  • Static members belong to the class itself and are accessed as ClassName.member, never through an instance.
  • static is real JavaScript, not erased at compile time — only type annotations and compile-time-only visibility keywords disappear from the emitted code.
  • The compiler treats a class as having a separate instance type and static (typeof ClassName) type, and rejects mixing the two.
  • Use static readonly for class-wide constants and # private static fields for state that must be truly hidden at runtime.
  • Static property initializers and static { ... } blocks run once, top to bottom, the moment the class is defined — before any instances exist.
  • Inside a static method, this refers to the class (or calling subclass), enabling polymorphic static factory methods.