Readonly<T> sounds like "this object can't be changed". It means "this object's own properties can't be reassigned" — one level deep, because the mapped type above only adds readonly to T's top-level keys and copies each value's type as-is. A nested object inside a Readonly settings object is just as writable as before, and TypeScript says nothing.
interface Settings {
theme: string;
limits: { maxUsers: number };
}
const settings: Readonly<Settings> = { theme: "dark", limits: { maxUsers: 10 } };
// settings.theme = "light"; // error: readonly -- as expected
settings.limits.maxUsers = 9999; // compiles: Readonly is only one level deep
console.log(settings.limits.maxUsers); // 9999A mapped type has the form { [K in Keys]: Transform }, iterating over a set of keys (often keyof SomeType) and applying a transformation to each property. This is exactly how TypeScript's own built-in utility types — Partial<T>, Readonly<T>, Pick<T, K> — are implemented in the standard library, not special compiler magic; anyone can write an equivalent mapped type from scratch.
A basic mapped type
keyof extracts a union of a type's property names, and the mapped type iterates over that union, producing a new type with the transformation applied to every one.
interface User {
name: string;
age: number;
}
type ReadonlyUser = {
[K in keyof User]: User[K]; // same shape, but see the readonly version below
};
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]; // this is essentially how Readonly<T> is built
};
type FrozenUser = MyReadonly<User>; // { readonly name: string; readonly age: number }That follows directly from MyReadonly above: T[K] is copied unchanged, so a property whose value is an object keeps a fully mutable object type. A mapped type can recurse, though — apply the same transformation to every nested object and the assignment becomes a compile error. (It is still a compile-time check only; use Object.freeze if you need the runtime to refuse the write.)
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const settings: DeepReadonly<Settings> = { theme: "dark", limits: { maxUsers: 10 } };
settings.limits.maxUsers = 9999;
// error TS2540: Cannot assign to 'maxUsers' because it is a read-only property.Modifiers: adding or removing readonly and optional
+/- prefixes on readonly or ? let a mapped type explicitly add or strip those modifiers, rather than just preserving whatever the source type already had.
type MyPartial<T> = {
[K in keyof T]?: T[K]; // essentially how Partial<T> is built
};
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // the -? REMOVES optionality (essentially Required<T>)
};Key remapping with as
The as clause (introduced in TypeScript 4.1) lets a mapped type rename keys during the transformation, not just transform their values — useful for generating a companion type with prefixed or derived property names.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }