Readonly Members & Parameter Properties
TypeScript provides two closely related features for working with class properties: the readonly modifier and parameter properties. Together, they let you write safer, more concise class definitions where immutability is enforced at compile time.
The readonly Modifier
Marking a class property readonly means it can only be assigned during initialization — either at the declaration site or inside the constructor. Any attempt to reassign it afterwards is a compile error.
class Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x; // ✅ assignment in constructor — allowed
this.y = y;
}
translate(dx: number, dy: number): Point {
// ❌ Error: Cannot assign to 'x' because it is a read-only property.
this.x += dx;
this.y += dy;
// ✅ Correct: return a new instance
return new Point(this.x + dx, this.y + dy);
}
}
const p = new Point(3, 4);
p.x = 10; // ❌ Error: Cannot assign to 'x' because it is a read-only property.readonly is a compile-time check only — at runtime the property is a regular writable property unless you also use Object.freeze() or a Proxy.readonly with Initializers
Readonly properties can also be initialized at the point of declaration. Once set, they cannot be changed — not even in the constructor.
class AppConfig {
readonly MAX_RETRIES = 3;
readonly BASE_URL = 'https://api.example.com';
readonly VERSION: string;
constructor(version: string) {
this.VERSION = version; // ✅ constructor assignment allowed
}
updateUrl() {
// ❌ Error: Cannot assign to 'BASE_URL' because it is a read-only property.
this.BASE_URL = 'https://other.example.com';
}
}
const config = new AppConfig('2.0.0');
console.log(config.VERSION); // '2.0.0'
console.log(config.MAX_RETRIES); // 3Parameter Properties
A parameter property is a TypeScript shorthand that simultaneously declares a class property and assigns the constructor argument to it. You add an access modifier (public, private, protected, or readonly) to a constructor parameter.
// Without parameter properties — verbose
class User {
public readonly id: string;
public name: string;
private email: string;
protected role: string;
constructor(id: string, name: string, email: string, role: string) {
this.id = id;
this.name = name;
this.email = email;
this.role = role;
}
}
// With parameter properties — concise, identical runtime behavior
class User {
constructor(
public readonly id: string,
public name: string,
private email: string,
protected role: string,
) {}
// No body needed — TypeScript generates all four assignments
}
const user = new User('u1', 'Alice', 'alice@example.com', 'admin');
console.log(user.id); // 'u1'
console.log(user.name); // 'Alice'Mixing Parameter Properties with Regular Properties
You can mix parameter properties with regular properties in the same class. Parameter properties appear in the constructor signature; derived or computed properties are declared in the class body.
class Product {
readonly sku: string;
discountedPrice: number;
constructor(
public readonly id: string,
public name: string,
public price: number,
private readonly taxRate: number = 0.2,
) {
// Computed from parameter properties
this.sku = `${id}-${name.slice(0, 3).toUpperCase()}`;
this.discountedPrice = price * 0.9; // 10% off
}
get priceWithTax(): number {
return this.price * (1 + this.taxRate);
}
applyDiscount(percent: number): void {
this.discountedPrice = this.price * (1 - percent / 100);
// Note: this.price is public (mutable), this.taxRate is readonly (immutable)
}
}
const p = new Product('001', 'Widget', 29.99);
console.log(p.sku); // '001-WID'
console.log(p.priceWithTax); // 35.988
console.log(p.discountedPrice); // 26.991readonly Arrays and Objects
readonly on a property only prevents reassigning the property reference. If the property holds an array or object, the contents of that array/object can still be mutated. To prevent deep mutation, use ReadonlyArray<T> or Readonly<T>.
class Team {
readonly members: string[]; // reference is readonly, but array is mutable!
constructor(members: string[]) {
this.members = members;
}
addMember(name: string) {
this.members.push(name); // ✅ Compiles! The array is mutable.
// this.members = [...this.members, name]; // ❌ Cannot assign to readonly
}
}
// To make the array itself immutable, use ReadonlyArray<T>:
class ImmutableTeam {
readonly members: ReadonlyArray<string>;
constructor(members: string[]) {
this.members = members;
}
addMember(name: string): ImmutableTeam {
// ❌ Error: Property 'push' does not exist on type 'readonly string[]'
// this.members.push(name);
// ✅ Return a new instance
return new ImmutableTeam([...this.members, name]);
}
}
// For objects, use Readonly<T>:
class Config {
readonly options: Readonly<{ timeout: number; retries: number }>;
constructor(timeout: number, retries: number) {
this.options = { timeout, retries };
}
withTimeout(ms: number): Config {
// ❌ Error: Cannot assign to 'timeout' because it is a read-only property
// this.options.timeout = ms;
// ✅ Return new instance
return new Config(ms, this.options.retries);
}
}readonly does NOT recursively make nested objects immutable. Use ReadonlyArray, Readonly<T>, or deep-freeze utilities for full immutability.readonly in Interfaces
The readonly modifier works in interfaces and type aliases too, providing immutability guarantees at the type level regardless of how the implementing class declares the property.
interface Vector2D {
readonly x: number;
readonly y: number;
magnitude(): number;
}
class Vec implements Vector2D {
constructor(
public readonly x: number,
public readonly y: number,
) {}
magnitude(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
add(other: Vector2D): Vec {
return new Vec(this.x + other.x, this.y + other.y);
}
}
// Even through the interface, readonly is enforced:
const v: Vector2D = new Vec(3, 4);
v.x = 10; // ❌ Error: Cannot assign to 'x' because it is a read-only property.Readonly Utility Type
TypeScript's built-in Readonly<T> utility type wraps all properties of a type in readonly, creating a deeply-immutable view of an object type. Note: this is shallow — nested objects are not made readonly.
interface UserSettings {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
// A frozen snapshot of settings
type FrozenSettings = Readonly<UserSettings>;
// Equivalent to:
// {
// readonly theme: 'light' | 'dark';
// readonly language: string;
// readonly notifications: boolean;
// }
function applySettings(settings: Readonly<UserSettings>) {
// Caller's settings cannot be mutated inside this function
settings.theme = 'dark'; // ❌ Error: Cannot assign to 'theme'
// Must return a new object
return { ...settings, theme: 'dark' } as UserSettings;
}
// Deep readonly — a recursive utility type
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};as const vs readonly
as const and readonly are related but different:
readonlyis a modifier on a class/interface property declarationas constis an assertion on a value that narrows the literal type and makes it readonly
// as const — makes a value deeply readonly at the type level
const DIRECTIONS = ['up', 'down', 'left', 'right'] as const;
// type: readonly ['up', 'down', 'left', 'right']
// DIRECTIONS[0] is 'up', not string
type Direction = typeof DIRECTIONS[number]; // 'up' | 'down' | 'left' | 'right'
DIRECTIONS.push('diagonal'); // ❌ Error: Property 'push' does not exist on readonly array
// as const on objects
const DEFAULT_CONFIG = {
timeout: 5000,
retries: 3,
baseUrl: 'https://api.example.com',
} as const;
// All properties are readonly AND narrowed to literal types
DEFAULT_CONFIG.timeout = 1000; // ❌ Error: Cannot assign to 'timeout'
// DEFAULT_CONFIG.timeout type is 5000, not numberReal-World Pattern: Immutable Value Objects
Immutable value objects — objects whose identity is defined by their value, not their reference — are a cornerstone of domain-driven design. TypeScript's readonly and parameter properties make them easy to implement.
class Money {
constructor(
public readonly amount: number,
public readonly currency: string,
) {
if (amount < 0) throw new Error('Amount cannot be negative');
if (!currency) throw new Error('Currency is required');
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error(`Cannot add ${this.currency} and ${other.currency}`);
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toString(): string {
return `${this.currency} ${this.amount.toFixed(2)}`;
}
}
const price = new Money(29.99, 'USD');
const tax = new Money(2.99, 'USD');
const total = price.add(tax);
console.log(total.toString()); // USD 32.98
console.log(price.equals(total)); // false
price.amount = 0; // ❌ Error — immutable!Real-World Pattern: Dependency Injection
Parameter properties are the standard pattern in NestJS, Angular, and other dependency-injection frameworks.
// NestJS service with parameter properties
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly emailService: EmailService,
private readonly logger: Logger,
) {}
// All three are automatically declared as private readonly class properties
async create(dto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(dto);
await this.userRepository.save(user);
await this.emailService.sendWelcome(user.email);
this.logger.log(`User created: ${user.id}`);
return user;
}
}
// Angular component with parameter properties
import { Component } from '@angular/core';
@Component({ selector: 'app-root', template: '...' })
export class AppComponent {
constructor(
private readonly authService: AuthService,
private readonly router: Router,
) {}
}Summary
readonly prevents property reassignment after construction — the property can only be set in its declaration or the constructor.
Parameter properties combine property declaration and constructor assignment into a single shorthand.
Use public readonly id: string in the constructor signature to declare, type, and assign all at once.
readonly on an array/object reference does not make the contents immutable — use ReadonlyArray<T> or Readonly<T>.
The Readonly<T> utility type wraps all properties of T in readonly shallowly.
as const makes a value's type literal and all properties readonly at the type level.
Value objects, configuration classes, and DI containers are the most common real-world uses of readonly and parameter properties.