1type User = { id: number; name: string; admin: boolean };2type Keys = keyof User; // "id" | "name" | "admin"3type AsText<T> = {4 [K in keyof T]: T[K] extends number ? string : T[K];5};6type Form = AsText<User>;7const f: Form = { id: 42, name: "Ada", admin: false };const f: Form = { id: "42", name: "Ada", admin: false };✗✓idnumbernamestringadminbooleanT[K] extends number ?yes: stringno: T[K]id?name?admin?"id""name""admin"id: numberid: stringname: stringname: stringadmin: booleanadmin: boolean
Back: form.ts, the code (line 4 is the mapped type with its conditional)Front: User (input) → the mapped type tray and the test → Form (output)Tokens: the keys, then each property with its type on its way through the testAt the back: form.ts, seven lines of TypeScript. In front: User, the input type; the mapped type, a tray that carries every key across; the conditional type, a test with a yes and a no lamp; and Form, the new type. keyof User lists the keys; [K in keyof T] makes one property per key; T[K] looks up each key's type, and T[K] extends number ? string : T[K] turns numbers into strings and keeps the rest. Form is { id: string; name: string; admin: boolean }, and it checks real values. The seven lines on the card are one real file. With id: 42 tsc 5.9 reports exactly the error shown; with id: "42" it reports none, and the emitted JavaScript runs in Node.js.

Keys

  1. User is an ordinary object type with three keys: id, name and admin. Each key has its own type: number, string and boolean. Line 1 writes it in code. Everything that follows builds a new type from this one.

The mapped type

The conditional type

Using the new type

What happens

;; step 1/9 · source
> 1  type User = { id: number; name: string; admin: boolean };

key id     type number
key name   type string
key admin  type boolean

The built-in utility type Partial<T> is a mapped type too: TypeScript defines it as type Partial<T> = { [P in keyof T]?: T[P] }; The ? after the key makes every property optional; readonly in front of [P in keyof T] makes every property read-only. When the type being tested is a bare type parameter, such as T in T extends string ? ..., a conditional type is applied to each member of a union separately ("distributive"). Here the tested type is T[K], not a bare T, so boolean is tested as a whole and stays boolean.