Most of the standard library lives in one syntactic form. Partial, Required, Readonly, Pick, Record: every one is a single mapped type, a few characters of modifier punctuation apart. Once you can read that punctuation, the utility types stop being magic. They become things you’d write yourself without thinking.

A mapped type iterates the keys of a type and produces a new property for each one. The skeleton is an identity transform:

type Identity<T> = { [K in keyof T]: T[K] };

type User = { id: number; name: string };
type Same = Identity<User>;
// { id: number; name: string }

keyof T yields the union 'id' | 'name', the mapped type distributes over that union, and T[K] is an indexed access that reads the original property type back out for each member. Nothing has changed yet. The power comes from what you do between the in clause and the T[K].

Modifiers, and the subtractive ones

You can attach readonly and ? to the produced properties. That much rebuilds two utility types directly:

type Partial<T> = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };

The interesting direction is the other one. A mapped type can remove a modifier with a - prefix, which strips readonly or optionality that the input already carries. This is the only way to express “make every field writable” or “make every field required” in the type system:

type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };

type Frozen = { readonly id: number; name?: string };

type Thawed = Mutable<Frozen>;
// { readonly removed → } { id: number; name?: string }

type Whole = Required<Frozen>;
// { readonly id: number; name: string }

A bare readonly or ? adds the modifier; -readonly and -? subtract it. Without the subtractive form, Required<T> would be impossible to write. No other operator clears optionality. One subtlety: -? also removes undefined from the property’s value type. That is usually what you want. It occasionally surprises people who only expected the question mark to vanish.

Rebuilding Pick

Pick shows the second axis of control. Instead of mapping over keyof T, map over a narrower union and read T[K] for each surviving key:

type Pick<T, K extends keyof T> = { [P in K]: T[P] };

type Contact = Pick<User, 'name'>;
// { name: string }

The K extends keyof T constraint is doing real work. It guarantees every member of K is a valid index into T, so T[P] never fails. Record is the same shape with the constraint loosened to keyof any and the value type supplied separately. Everything so far keeps the original key names. The next step changes them.

Key remapping with as

TypeScript 4.1 added an as clause to the in binding. Whatever type you write after as becomes the produced key, computed per iteration with K in scope. The straightforward use is renaming:

type Prefixed<T> = {
  [K in keyof T as `data_${string & K}`]: T[K]
};

type Tagged = Prefixed<{ id: number; ok: boolean }>;
// { data_id: number; data_ok: boolean }

The string & K intersection is load-bearing. keyof T can include number and symbol, neither of which is assignable to the string slot a template literal expects. You intersect with string to keep only the string keys and satisfy the constraint.

The sharper trick is filtering. A mapped type drops any key it remaps to never. So you compute, per key, whether to keep it, emitting the original key name when yes and never when no:

type OmitByValue<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K]
};

type Mixed = {
  id: number;
  name: string;
  createdAt: number;
};

type NoNumbers = OmitByValue<Mixed, number>;
// { name: string }

Both numeric keys remap to never and disappear from the result; the string key remaps to itself and survives. Invert the conditional and you get PickByValue. This is also how a hand-written Omit works under the hood. Remap the keys you want gone to never:

type Omit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P]
};

type WithoutId = Omit<User, 'id'>;
// { name: string }

The as never removal applies key by key, so filtering and transforming happen in the same pass. You can rename surviving keys and delete others in one mapped type.

Synthesizing keys from templates

Combine remapping with template literals and Capitalize, and the type system will generate API surface for you. The canonical example derives getters from a state shape:

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

type Store = { count: number; label: string };

type StoreGetters = Getters<Store>;
// {
//   getCount: () => number;
//   getLabel: () => string;
// }

Each key is rewritten through the get${Capitalize<...>} template while the value type is rebuilt as a thunk returning the original type. Event handlers fall out of the same pattern. Because the value type can reference K, the synthesized method stays aware of which property it came from:

type Handlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]:
    (next: T[K]) => void
};

type FormHandlers = Handlers<{ email: string; age: number }>;
// {
//   onEmailChange: (next: string) => void;
//   onAgeChange: (next: number) => void;
// }

One caveat: if two distinct source keys produce the same remapped key, the results collapse. The value types union together rather than colliding into an error. Template synthesis with prefixes rarely hits this. A remap that discards information, such as lowercasing, does.

Why this composes

Three primitives are at play, and they nest cleanly. keyof produces the union of keys. The mapped type distributes over that union, running its body once per member independently. Indexed access T[K] reads the value type back out. The as clause sits on top, free to compute an entirely new key from the key currently in scope, including never, which deletes. Because each iteration is independent, you reason about a single representative key and trust the distribution to handle the rest, the same way you’d reason about one element of a .map() callback.

That independence is also the limit. A mapped type sees one key at a time and can’t relate two keys to each other or count them. The moment you need that, you’re in recursive-conditional territory, the engine behind parsing strings with template literal types, where the same template-literal keys you’re synthesizing here get taken apart character by character.