← thecodex.expert · The Codex Family of Knowledge
TypeScript

Mapped Types

Partial<T>, Readonly<T>, and Pick<T> all look like built-in magic — they’re actually ordinary mapped types, defined in TypeScript’s own standard library, that you can write yourself.

TypeScript 6.0 Partial, Readonly, Pick Last verified:
Where this bites you

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.

TypeScriptshallow_readonly_bug.ts
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);   // 9999
Canonical Definition

A 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.

TypeScriptbasic_mapped.ts
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.)

TypeScriptdeep_readonly_fix.ts
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.

TypeScriptmodifiers.ts
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.

TypeScriptkey_remapping.ts
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }

Sources

1
Microsoft. TypeScript Handbook, "Mapped Types", typescriptlang.org/docs/handbook/2/mapped-types.html.