Readonly<Type>

Marks every property of Type as readonly so the type checker blocks reassignment.

Signature
type Readonly<Type> = { readonly [Property in keyof Type]: Type[Property] }
  • Added in TypeScript 2.1

Parameters

ParameterTypeDescription
$Typeany typeThe source type whose properties are copied with the `readonly` modifier added.

Return value

An object type with the same properties as Type, each marked readonlyA mapped type over keyof Type in which every property carries the `readonly` modifier.

Errors

Assigning to a property raises a compile-time error ("Cannot assign to 'x' because it is a read-only property"). Nothing is raised at runtime.

Changelog

  • PHP 3.4`as const` assertions arrived, giving a per-expression alternative that also makes nested literals and array elements readonly — something Readonly<Type> does not do.

Readonly copies every property of Type and adds the readonly modifier, so the checker rejects assignments to those properties.

Compile-time only — nothing is frozen

This is the single most important fact about it. readonly is erased along with the rest of the type information; the emitted JavaScript is an ordinary mutable object. Object.isFrozen() returns false, and any code that reaches the object through a non-readonly alias — a cast, an any, a JSON round-trip, a third-party callback — will mutate it happily.

Object.freeze() is the runtime counterpart: it actually prevents writes (silently in sloppy mode, with a TypeError in strict mode). Its return type is Readonly<T>, so freezing gives you both guarantees at once. Use Readonly<T> when you only need to stop your own code from mutating a parameter, and Object.freeze() when untrusted or distant code holds the reference.

What trips people up

It is shallow. Readonly<{ tags: string[] }> stops you replacing tags, but tags.push("x") is still allowed. Use readonly string[] or ReadonlyArray for the elements.

Readonly-ness is not checked on assignment. A Readonly<T> is assignable to a T, so passing one into a function that mutates compiles without complaint.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Declare a function parameter the implementation must not mutate.
  • Publish a shared constants object whose fields the rest of the codebase cannot reassign.
  • Model server-supplied state in a store where updates must go through a reducer.

Frequently asked questions

Does Readonly freeze the object at runtime?

No. `Readonly<T>` is erased during compilation and produces an ordinary mutable JavaScript object — `Object.isFrozen()` returns false. Use `Object.freeze()` if you need the write to actually fail at runtime.

What is the difference between Readonly<T> and as const?

`Readonly<T>` is a shallow type transform applied to a named type, while `as const` is an assertion on a literal expression that makes the whole literal deeply readonly and narrows values to their literal types. Use `as const` for inline constant data, `Readonly<T>` for parameters and interfaces.

Why can I still pass a Readonly value to a function that mutates it?

Because readonly modifiers are not considered when checking assignability between object types — `Readonly<T>` and `T` are mutually assignable. The checker only blocks assignments made directly through a readonly-typed reference.

More examples

Shallow: the array inside is still mutable

The first log shows `tags` grown to `[ 'ts', 'types' ]` — `Readonly<Article>` protected the `tags` binding, not the array it points at. The second half declares the element type as `readonly string[]`, where `push` is not in the type at all and only non-mutating methods like `concat` remain; the log prints a new array without touching the original.

Example · typescript
interface Article {
  title: string;
  tags: string[];
}

const article: Readonly<Article> = { title: "Mapped Types", tags: ["ts"] };

// article.title = "x";   // tsc error: read-only property
article.tags.push("types"); // allowed — Readonly stopped at the property
console.log(article);

interface DeepArticle {
  readonly title: string;
  readonly tags: readonly string[];
}

const deep: DeepArticle = { title: "Mapped Types", tags: ["ts"] };
// deep.tags.push("types"); // tsc error: push does not exist on readonly string[]
console.log(deep.tags.concat("types"));

Object.freeze gives the type and the runtime guarantee

In a module (always strict mode) this prints `write threw TypeError` and then `3`. Note that `CONFIG` was accepted by a parameter typed `{ retries: number }` — readonly-ness is not enforced across assignment, so only the freeze stopped the write. Had `CONFIG` been a plain `Readonly<...>` annotation, the same call would have printed `write succeeded` and `99`.

Example · typescript
const CONFIG = Object.freeze({
  retries: 3,
  timeoutMs: 5000,
});
// CONFIG's inferred type is Readonly<{ retries: number; timeoutMs: number }>

function tryWrite(target: { retries: number }): string {
  try {
    target.retries = 99;
    return "write succeeded";
  } catch {
    return "write threw TypeError";
  }
}

console.log(tryWrite(CONFIG));
console.log(CONFIG.retries);

Discussion

  • Be the first to comment on this lesson.