Readonly<Type>
Marks every property of Type as readonly so the type checker blocks reassignment.
type Readonly<Type> = { readonly [Property in keyof Type]: Type[Property] }- Added in TypeScript 2.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$Type | any type | The 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
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.
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`.
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