On this page
Conditional and mapped types
The shape of advanced type manipulation
In the previous lessons you used the built-in utility types. In this lesson you learn how those utilities are built — and how to write your own. The two key ingredients are conditional types and mapped types. Together with template literal types, they give you a complete language for transforming types at compile time.
Conditional types
A conditional type has the form T extends U ? X : Y. If T is assignable to U, the whole expression resolves to X; otherwise it resolves to Y.
type IsArray<T> = T extends unknown[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // falseConditional types compose naturally with generics:
type Unwrap<T> = T extends Promise<infer V>
? V
: T extends Array<infer V>
? V
: T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<number[]>; // number
type C = Unwrap<boolean>; // booleanDistributive behavior
When the checked type is a naked (un-wrapped) type parameter, TypeScript automatically distributes the conditional over each union member:
type WrapInArray<T> = T extends unknown ? T[] : never;
type Result = WrapInArray<string | number>;
// string[] | number[] — distributed over the unionTo prevent distribution, wrap the parameter in a one-element tuple:
type NonDistributive<T> = [T] extends [unknown] ? T[] : never;
type Result = NonDistributive<string | number>;
// (string | number)[] — the entire union becomes the element typeMapped types
A mapped type iterates over a union of keys and produces a new object type:
type ReadonlyVersion<T> = {
readonly [K in keyof T]: T[K];
};This is exactly how the built-in Readonly<T> is implemented. The [K in keyof T] syntax means "for each key K in the keys of T". You can add or remove modifiers:
// Remove readonly and optional modifiers
type Mutable<T> = {
-readonly [K in keyof T]-?: T[K];
};The - prefix before readonly or ? removes that modifier. The + prefix (default) adds it.
Mapping over a custom union
Mapped types are not limited to keyof T. You can iterate over any string (or number/symbol) union:
type StatusColors = "success" | "warning" | "error" | "info";
type AlertStyles = {
[S in StatusColors]: {
background: string;
icon: string;
};
};Key remapping with `as`
TypeScript lets you rename keys during a mapping using the as clause:
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface Profile {
name: string;
age: number;
}
type ProfileSetters = Setters<Profile>;
// { setName: (value: string) => void; setAge: (value: number) => void }You can also use as with a conditional to filter out keys by remapping them to never:
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
score: number;
}
type NumberFields = PickByValue<Mixed, number>; // { id: number; score: number }
type StringFields = PickByValue<Mixed, string>; // { name: string }Template literal types
Template literal types let you build new string literal types by combining existing ones, using the same syntax as JavaScript template literals:
type HttpMethod = "get" | "post" | "put" | "delete";
type ApiPath = "/users" | "/posts" | "/comments";
type Endpoint = `${Uppercase<HttpMethod>} ${ApiPath}`;
// "GET /users" | "GET /posts" | ... (12 combinations total)Combined with mapped types, this enables powerful API-contract modeling:
type ApiClient = {
[M in HttpMethod]: (path: string, body?: unknown) => Promise<unknown>;
};Building a custom `DeepPartial`
The built-in Partial<T> only goes one level deep. Here is a recursive version using conditional and mapped types:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface AppState {
user: {
profile: {
name: string;
bio: string;
};
settings: {
theme: "dark" | "light";
notifications: boolean;
};
};
}
type PartialState = DeepPartial<AppState>;
// All nested properties are optional:
const patch: PartialState = {
user: {
settings: {
theme: "dark",
// notifications omitted — valid!
},
},
};Building a `Paths` type
A more advanced example — computing all dot-notation paths of a nested object:
type Paths<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? Paths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
: `${Prefix}${K}`;
}[keyof T & string];
interface Form {
user: {
name: string;
address: {
city: string;
zip: string;
};
};
}
type FormPaths = Paths<Form>;
// "user" | "user.name" | "user.address" | "user.address.city" | "user.address.zip"This is the foundation of libraries like React Hook Form and Zod that provide type-safe field paths.
Practice
- Basic mapped type: Write a
Nullable<T>mapped type that makes every property ofTacceptnullin addition to its original type. Test it on aUserinterface. - Key remapping: Write a
EventHandlers<T>type that, given an interface, produces a new interface where each keykis renamed toonK(capitalized) and each value type becomes(payload: OriginalValueType) => void. - Conditional type: Write
IsPromise<T>— a conditional type that resolves totrueifTis aPromise<unknown>, andfalseotherwise. Test it withPromise<string>,string, andPromise<number[]>. - Template literal: Define a union of REST resource names (
"user" | "post" | "comment"). Use a mapped type with a template literal to produce a record type where keys aregetUsers,getPosts,getComments(pluralized and prefixed withget) and values are() => Promise<unknown>.
Next lesson: classes and decorators — object-oriented TypeScript and the TC39 decorator syntax.
// Basic mapped type — iterate over every key of T
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Key remapping with "as" — rename keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }
// Filter keys with "as" + conditional (remove never to exclude keys)
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<User>;
// { name: string; email: string } (id excluded — it's a number)
// Template literal types — compose string types
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Full event map using mapped + template literal
type EventMap = {
[E in EventName as `on${Capitalize<E>}`]: (event: Event) => void;
};
// { onClick: ...; onFocus: ...; onBlur: ... }
// Custom DeepReadonly using conditional + mapped types
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
server: { host: string; port: number };
flags: string[];
}
type ImmutableConfig = DeepReadonly<Config>;
// server.host and server.port are readonly, flags is ReadonlyArray<string>
// Distributive conditional — flatten or wrap union members
type Nullable<T> = T extends unknown ? T | null : never;
type IdsOrNull = Nullable<number | string>;
// number | null | string | null => number | string | null
Sign in to track your progress