Readonly & Static Members
Two modifiers shape how class members behave beyond simple visibility: readonly makes a field immutable after initialization, and static attaches a member to the class itself rather than to instances. They solve different problems — immutability versus shared state — but often appear together, for example in a static readonly constant. This page covers both, including static blocks introduced in TypeScript 4.4 and how static interacts with inheritance.
readonly fields
A readonly field can be assigned only at its declaration or inside the constructor. After construction, any attempt to reassign it is a compile-time error. This is a TypeScript-only check; the value is still mutable JavaScript at runtime.
class Circle {
readonly radius: number;
readonly pi = 3.14159;
constructor(radius: number) {
this.radius = radius; // ✅ allowed in constructor
}
scale(factor: number): void {
this.radius *= factor; // ❌ Error: Cannot assign to 'radius'
}
}
readonly protects against accidental mutation but does not deep-freeze objects: a readonly array can still have elements pushed unless its type is readonly T[]. Use it to signal intent and catch bugs early.
static members
A static member belongs to the class, not to any instance. You access it through the class name. Static members are ideal for constants, counters, and utility methods that do not depend on instance state.
class MathUtils {
static readonly PI = 3.14159;
static instances = 0;
static square(n: number): number {
return n * n;
}
}
MathUtils.PI; // 3.14159
MathUtils.square(5); // 25
There is exactly one copy of each static member, shared by all code. Note you call MathUtils.square(5), not new MathUtils().square(5) — static methods are not available on instances, and vice versa.
Static and instance counters
A common pattern combines a static field with the constructor to track how many instances exist.
class Widget {
static count = 0;
readonly id: number;
constructor() {
this.id = ++Widget.count;
}
}
new Widget(); // id 1
new Widget(); // id 2
Widget.count; // 2
Each new Widget() increments the shared Widget.count and stamps the instance with a unique readonly id. The instance field is per-object; the static field is global to the class.
Static blocks
TypeScript 4.4+ supports static initialization blocks for complex static setup that needs statements, not just an expression. They run once when the class is defined and can access private static members.
class Registry {
static items: Map<string, number>;
static {
this.items = new Map();
this.items.set("default", 0);
}
}
The static { ... } block runs in declaration order and is the right place for logic that a single initializer expression cannot express.
static vs instance — comparison
| Feature | static member | Instance member |
|---|---|---|
| Belongs to | The class | Each object |
| Accessed via | ClassName.member | this.member / obj.member |
| Copies in memory | One, shared | One per instance |
Can use this as | The class | The instance |
| Good for | Constants, factories, counters | Per-object state |
Static members are inherited and can be overridden by subclasses, but they are resolved through the class, not the prototype chain of instances. Avoid relying on
thisinside static methods when a subclass might call them in surprising ways.
Static members and inheritance
Subclasses inherit static members and can reference them, and this inside a static method refers to the class it was called on.
class Base {
static create(): Base {
return new this(); // `this` is the subclass when called as Sub.create()
}
}
class Sub extends Base {}
const s = Sub.create(); // instance of Sub, typed as Base
Using new this() in a static factory makes the method polymorphic across subclasses. Combine static readonly for shared constants that must never change.
Best Practices
- Mark fields
readonlywhenever they should not change after construction to document intent. - Use
readonly T[](not just areadonlyfield) to prevent array element mutation. - Reserve
staticfor state and behavior that genuinely belong to the class, not an instance. - Use
static readonlyfor class-level constants instead of loose module variables when they’re conceptually tied to the class. - Prefer static blocks over a separate init function for one-time static setup.
- Remember
readonlyis compile-time only; useObject.freezeor#privatefor runtime guarantees. - Avoid sprawling static state — it is effectively global and complicates testing.