Skip to main content

Readonly Utility Type

Immutability prevents bugs by ensuring data isn't accidentally modified after creation. Readonly enforces this at the type level.

The problem

In JavaScript, passing an object into a function passes a reference. If the function mutates the object, those changes leak out to the caller. TypeScript lets us explicitly declare that an object should not be touched.

Readonly<Type> constructs a type with all properties set to readonly, meaning they cannot be reassigned.

interface Config {
endpoint: string;
retries: number;
}

const config: Readonly<Config> = {
endpoint: "https://api.example.com",
retries: 3,
};

// config.retries = 5; // Error: Cannot assign to 'retries' because it is a read-only property.

Readonly arrays

Arrays are notorious for accidental mutations (e.g., using .push(), .pop(), or .sort()). TypeScript provides a dedicated syntax for read-only arrays.

function processIds(ids: readonly string[]) {
// ids.push("123"); // Error: Property 'push' does not exist on type 'readonly string[]'

// Safe array methods that return new arrays are allowed
const mapped = ids.map(id => id.toUpperCase());
}

You can also use ReadonlyArray<string>, which is functionally identical to readonly string[].

Readonly is Shallow

Just like Partial and Required, Readonly is shallow.

interface State {
user: { name: string };
}

const state: Readonly<State> = { user: { name: "Alice" } };

// state.user = { name: "Bob" }; // Error: Cannot assign to 'user'
state.user.name = "Bob"; // Valid! The nested object is not readonly.

If you need deep immutability, look into as const or recursive custom utility types.

Use Readonly to prevent accidental mutation of objects and arrays. Remember that the immutability is strictly shallow and enforced only at compile time.