TypeScript Advanced #2: Mapped types
In #1 keyof and typeof we saw how to gather an object’s keys into a union. Stack one more layer on top and you get mapped types that transform an entire object type. Utility types like Partial, Required, and Readonly are built from this.
The basic shape #
A mapped type is this one-line syntax.
type MyType<T> = {
[K in keyof T]: T[K];
};
// MyType<{ a: string; b: number }>
// = { a: string; b: number }
[K in keyof T] is the key part. It means “for each key K of T.” The T[K] inside is the indexed access from #1 — the value type pulled by that key.
The example above just clones the input, but substitute a different type for T[K] and the transformation begins.
type Stringify<T> = {
[K in keyof T]: string;
};
type S = Stringify<{ id: number; age: number; ok: boolean }>;
// { id: string; age: string; ok: string }
Building Partial ourselves
#
Partial<T> is a built-in type that makes every field optional. Add one modifier and you can write it yourself.
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type Patch = MyPartial<{ id: string; name: string; age: number }>;
// { id?: string; name?: string; age?: number }
The ? is the trick. Adding it after the key position like [K in keyof T]? makes every field optional.
Required — make every field required
#
The opposite direction. Add -? to the modifier to remove the optional marker.
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
type Strict = MyRequired<{ id?: string; name?: string }>;
// { id: string; name: string }
? and -? form a pair. The built-in Partial/Required are defined exactly this way. What once seemed like magic worth memorizing turns out to be a one-line mapped type.
Readonly works the same way
#
To make values read-only, add the readonly modifier.
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type Frozen = MyReadonly<{ id: string; name: string }>;
// { readonly id: string; readonly name: string }
-readonly works too. Sometimes used when an external library type is readonly and you want to undo it.
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type M = Mutable<Readonly<{ id: string }>>;
// { id: string }
Pick and Omit are family too
#
Pick<T, K> and Omit<T, K> are also mapped types. The only difference is where the set of keys to iterate comes from.
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type UserBasic = MyPick<{ id: string; name: string; age: number }, 'id' | 'name'>;
// { id: string; name: string }
[P in K] — iterates over the union K passed in from outside, not keyof T. You can see that mapped types are really a tool that “uses any union as keys.”
Omit takes a tiny bit more work. Use the built-in Exclude to subtract the key set.
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
type WithoutAge = MyOmit<{ id: string; name: string; age: number }, 'age'>;
// { id: string; name: string }
Exclude<U, V> removes members of V from union U. We’ll write it ourselves in #3 conditional types.
Renaming keys — the as clause
#
From here on are features that aren’t in the built-in utilities — features that make sense to write yourself. With an as clause you can rename keys.
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserSetters = Setters<{ name: string; age: number }>;
// {
// setName: (value: string) => void;
// setAge: (value: number) => void;
// }
Three tools are combined here.
- Template literal type —
`set${...}`composes a string type. We’ll see this in earnest in #4. Capitalize<S>— built-in helper. Capitalizes the first letter.string & K— filters out symbol/number keys, keeping only string keys.
We turn the name key into setName and turn its value type into a setter function. This kind of auto-generation pattern shows up often when building libraries.
as + never = removing a key
#
If never appears in the as slot, that key disappears from the result type.
type FunctionsOnly<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
type Mixed = {
id: string;
name: string;
greet: () => void;
fetch: () => Promise<void>;
};
type Methods = FunctionsOnly<Mixed>;
// { greet: () => void; fetch: () => Promise<void> }
If the value type isn’t a function, the key gets remapped to never and is removed. This unlocks another level of expressive power for mapped types.
Recap of built-in utilities — what’s now visible #
At this point, you can write more than half of the utility types from Basics #7 yourself.
| Utility | Definition |
|---|---|
Partial<T> | { [K in keyof T]?: T[K] } |
Required<T> | { [K in keyof T]-?: T[K] } |
Readonly<T> | { readonly [K in keyof T]: T[K] } |
Pick<T, K> | { [P in K]: T[P] } |
Record<K, V> | { [P in K]: V } |
It’s meaningful that Record<K, V> is also a mapped type. Defined in one line:
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
type Roles = MyRecord<'admin' | 'user' | 'guest', boolean>;
// { admin: boolean; user: boolean; guest: boolean }
K extends keyof any shows up — that means “string | number | symbol — anything that can be an object key.” It’s a common idiom.
One more level — going deeper #
When objects are nested, sometimes you want to transform recursively. You can do that by calling a mapped type on itself.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
type Settings = {
theme: { color: string; font: string };
user: { id: string; flags: { admin: boolean } };
};
type Frozen = DeepReadonly<Settings>;
// readonly all the way down
T[K] extends object ? ... : ... is the conditional type we’ll cover in earnest in #3. Inside the mapped type, we created the branch “if the value is an object, map again; otherwise pass through.” When these two tools meet, expressiveness explodes.
The same pattern gives you DeepPartial, also common.
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};Common in cases like partial updates, settings overrides, and form patches. Libraries adopt this shape often.
Pitfall — functions and arrays count as object
#
T[K] extends object is a surprisingly broad condition. Functions, arrays, Date, and Map are all objects. When writing something like DeepReadonly, recursing into functions can break it. The usual fix is to explicitly exclude primitives and functions.
type Primitive = string | number | boolean | null | undefined | bigint | symbol;
type DeepReadonly<T> = T extends Primitive | Function
? T
: { readonly [K in keyof T]: DeepReadonly<T[K]> };By this point, you’ll want to be comfortable with conditional types. We get hands-on with them in the next post.
Wrap-up #
What this post covered:
- Mapped type basics —
{ [K in keyof T]: T[K] } - Modifiers —
?/-?(optional),readonly/-readonly(readonly) Partial,Required,Readonly,Pick,Recordare all mapped types- Renaming keys with the
asclause — patterns like`as `set${...}` - Removing keys with
as ... never - Calling itself for deep recursive transforms (DeepReadonly/DeepPartial)
- Carefully exclude primitives/functions when recursing
In the next post (#3 Conditional types and infer), we cover the T extends U ? X : Y syntax that briefly appeared above in earnest. Once we add the infer keyword, we’ll write built-ins like ReturnType, Parameters, and Awaited ourselves.