Readonly Class Properties in TypeScript

The readonly modifier prevents reassignment to a class property after the constructor finishes. Learn how to use it, when it helps, and how it differs from const.

7 min read

A readonly class property can be assigned exactly once: in the field declaration or in the constructor. After the constructor finishes, TypeScript blocks any further assignment.

typescripttypescript
class Greeter {
  readonly name: string;
 
  constructor(name: string) {
    this.name = name;
  }
 
  rename(newName: string) {
    this.name = newName;
  }
}

TypeScript catches this assignment attempt and produces the following compile error. The readonly modifier prevents any write to the property outside of the constructor body.

texttext
Cannot assign to 'name' because it is a read-only property.

The constructor assignment is fine. The attempt inside rename is not. TypeScript catches it at compile time and refuses to produce JavaScript for the broken code.

Where Readonly Applies

The constructor is the only place you can assign to a readonly field after its declaration. You can assign as many times as you want inside the constructor:

typescripttypescript
class User {
  readonly id: number;
 
  constructor(rawId: string | number) {
    if (typeof rawId === "string") {
      this.id = parseInt(rawId, 10);
    } else {
      this.id = rawId;
    }
  }
}

Both branches of the if-statement assign to this.id. TypeScript accepts this because the constructor is the one place where readonly fields can be set dynamically. This is useful when the field value depends on conditional logic during setup.

After the constructor returns, the field is locked:

typescripttypescript
const user = new User("42");
user.id = 99;

TypeScript reports this error at compile time, since the constructor already finished running and the id field is now permanently locked against reassignment:

texttext
Cannot assign to 'id' because it is a read-only property.

External code cannot change the id after the object is constructed.

Readonly with an Initializer

You can initialize a readonly field directly at the declaration site. When you do, the initializer is the one and only assignment - even the constructor cannot change it:

typescripttypescript
class Config {
  readonly apiVersion = "v2";
  readonly maxRetries = 3;
}
 
const config = new Config();
console.log(config.apiVersion, config.maxRetries);

This prints v2 3, the two values set directly on the field declarations. No constructor is needed at all here.

texttext
v2 3

A readonly field with an initializer is fully locked. No constructor body or method can reassign it.

Readonly vs Const

These two keywords solve similar problems in different places:

constreadonly
Where usedVariable declarationsClass properties, interface members
ScopeBlock-scopedInstance-scoped
Runtime behaviorCannot be reassigned (JS enforces)Compile-time check only (erased)
Can use with objectsYes, but object is still mutableYes, but object is still mutable

Use const for module-level constants. Use readonly for per-instance values that should not change after construction.

Readonly Is Shallow

The readonly modifier only blocks reassignment of the property reference. If the value is an object or array, its contents can still change:

typescripttypescript
class Team {
  readonly members: string[] = ["Alice", "Bob"];
 
  addMember(name: string) {
    this.members.push(name);
  }
 
  replaceTeam(newMembers: string[]) {
    this.members = newMembers;
  }
}

The push call works because it mutates the existing array without changing the reference stored in members. The replaceTeam method fails because it tries to assign a new array to members:

texttext
Cannot assign to 'members' because it is a read-only property.

For a truly immutable array, use the ReadonlyArray utility type instead of a plain array type. This blocks mutations to the array contents as well as reassignment of the property itself.

Readonly with Parameter Properties

Combine readonly with parameter properties to create a field that is set once through the constructor and never changes:

typescripttypescript
class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}
}
 
const p = new Point(10, 20);
console.log(p.x, p.y);

This prints 10 20, since the public readonly prefix declares and assigns both fields in one step. Now try to change one of the fields after construction:

typescripttypescript
p.x = 30;

TypeScript rejects this on its own, separate from the working example above, because the field was declared readonly. The compiler blocks the assignment with the following error and never emits JavaScript for this line:

texttext
Cannot assign to 'x' because it is a read-only property.

This pattern is very common for value objects, data transfer objects, and any class where the identity should be fixed at creation time. The public readonly combination gives you a field that anyone can read but no one can change.

When to Use Readonly

Use readonly for fields that should not change after the object is created:

  • Identity fields like id, uuid, or createdAt.
  • Injected dependencies that are set once by a framework or factory.
  • Configuration values that are fixed per instance.
  • Derived values that are computed in the constructor and cached.

Do not use readonly for fields that genuinely need to change over the object's lifetime, temporary working state, or counters. Also, a getter with no setter is automatically readonly, so you do not need the keyword there.

Common Mistakes

Expecting deep immutability. Readonly only blocks this.field = newValue. It does not block this.field.push(item) or this.field.nested = value. If you need deep immutability, combine readonly with Readonly utility types or as const.

Trying to use const on a class property. Const only works on variables in block or module scope. Inside a class body, only readonly is available.

Assigning a readonly field in a method called from the constructor. TypeScript does not track assignments through method calls during construction. Even if an init method assigns the field, TypeScript will still report that the field is not definitely assigned. Assign readonly fields directly in the constructor body.

For more on how classes control access, see public private and protected in TypeScript. For enforcing contracts on class shapes, see implements keyword in TypeScript.

Rune AI

Rune AI

Key Insights

  • readonly prevents assignment to a class property everywhere except in the constructor.
  • It is a compile-time check only. At runtime, readonly properties are plain writable properties.
  • readonly is for class properties and interface members. const is for variables.
  • readonly is shallow. The value can still be mutated if it is an object or array.
  • Combine readonly with access modifiers like public readonly for fields set once via the constructor.
RunePowered by Rune AI

Frequently Asked Questions

Does readonly make a property deeply immutable?

No. readonly only prevents reassignment of the property itself. If the property holds an object or array, you can still mutate the contents of that object or array. Use Readonly utility types or as const for deeper immutability.

Can I set a readonly property more than once in the constructor?

Yes. The constructor can assign to readonly properties as many times as needed. Only assignments outside the constructor are blocked. This lets you do conditional setup logic before locking the value.

What is the difference between readonly and const?

const is for variables. readonly is for class properties and interface members. You use const when declaring a top-level variable that will not be reassigned. You use readonly when declaring a class field that should be set once and then stay fixed.

Conclusion

The readonly modifier locks a class property after the constructor finishes. It is a compile-time guard that prevents accidental reassignment and signals to other developers that a value should not change. Use it for fields that represent identity, configuration, or injected dependencies. Remember that readonly is shallow: it blocks reassignment of the property but does not freeze the value itself.