TypeScript Advanced Types
Learn conditional types, mapped types, template literals, and advanced pattern matching.
TL;DR
- 01Use conditional types for type-level if-else logic.
- 02Use mapped types to transform keys and values in existing types.
- 03Use template literal types for string manipulation at the type level.
Tips
- 01Use conditional types with infer to extract and reuse parts of complex types in your type transformations.
Warnings
- 01Advanced types are powerful but can become hard to read — document complex type transformations and keep them focused on single concerns.
Conditional Types
Write type-level if-else logic using the T extends X ? Y : Z ternary syntax.
type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<number>; // falseUse infer to capture and reuse a type nested inside a generic structure.
type Flatten<T> = T extends Array<infer U> ? U : T; type Str = Flatten<string[]>; // string type Num = Flatten<number>; // numberExtract function return types without importing the function itself.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type NumReturn = ReturnType<() => number>; // number type StrReturn = ReturnType<() => string>; // stringDistribute conditional types over union members automatically.
type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // string[] | number[]Prevent distribution by wrapping T in a tuple when you need one result.
type IsUnion<T> = [T] extends [infer U] ? (U extends T ? false : true) : false; type A = IsUnion<string | number>; // true type B = IsUnion<string>; // false
Mapped Types
Transform every key in an object type by iterating over keyof.
type MyReadonly<T> = { readonly [K in keyof T]: T[K]; }; type User = { name: string; age: number }; type ReadonlyUser = MyReadonly<User>; // { readonly name: string; readonly age: number }Make all properties optional by adding ? in a mapped type.
type Optional<T> = { [K in keyof T]?: T[K]; }; type PartialUser = Optional<User>; // { name?: string; age?: number }Rename keys with the as clause using template literal types.
type Getters<T> = { [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]; }; type UserGetters = Getters<User>; // { getName: () => string; getAge: () => number }Filter keys by combining mapped types with conditional types.
type StringKeys<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; type User = { id: number; name: string; email: string }; type StringOnly = StringKeys<User>; // { name: string; email: string }Build a Setters utility by inverting Getters with the mapped as pattern.
type Setters<T> = { [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void; }; type UserSetters = Setters<User>; // { setName: (value: string) => void; setAge: (value: number) => void }
Template Literal Types
Build string types at the type level using backtick template syntax.
type Event<T extends string> = `on${Capitalize<T>}`; type ClickEvent = Event<"click">; // "onClick" type ChangeEvent = Event<"change">; // "onChange"Combine a union with a template to generate all string combinations.
type Path = "user" | "post" | "comment"; type GetRoute = `GET /${Path}`; // "GET /user" | "GET /post" | "GET /comment"Extract the prefix of a string type with infer inside a template.
type GetPrefix<T extends string> = T extends `${infer Prefix}_${string}` ? Prefix : never; type P = GetPrefix<"user_id">; // "user" type Q = GetPrefix<"post_slug">; // "post"Split a string type into a tuple using recursive conditional types.
type Split<T extends string, D extends string> = T extends `${infer F}${D}${infer R}` ? [F, ...Split<R, D>] : [T]; type Parts = Split<"a-b-c", "-">; // ["a", "b", "c"]Build CSS-property-like types from a set of base names.
type Side = "top" | "right" | "bottom" | "left"; type Margin = `margin-${Side}`; // "margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Advanced Utility Patterns
Build DeepPartial to make every nested property optional recursively.
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;Build DeepReadonly to freeze every level of a nested object type.
type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;Check type assignability at the type level with a boolean predicate.
type IsAssignableTo<T, U> = T extends U ? true : false; type A = IsAssignableTo<"hello", string>; // true type B = IsAssignableTo<number, string>; // falseBuild RequireKeys to make selected optional fields required.
type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>; type User = { id?: number; name?: string; email?: string }; type UserWithId = RequireKeys<User, "id">; // { id: number; name?: string; email?: string }Build a Validator type that maps each key to a validation function.
type Validator<T> = { [K in keyof T]: (value: T[K]) => boolean; }; // Usage: const userValidator: Validator<User> = { name: (v) => v.length > 0 };
Practical Applications
Build type-safe API response types using conditional types on status.
type ApiResponse<T extends "success" | "error"> = T extends "success" ? { status: 200; data: unknown } : { status: 400; error: string }; type Ok = ApiResponse<"success">; // { status: 200; data: unknown }Create a database query builder type using mapped types with as.
type SelectBuilder<T> = { [K in keyof T as `select${Capitalize<K & string>}`]: () => T[K]; }; // SelectBuilder<User> gives: { selectName: () => string; selectId: () => number }Extract promise resolution types for async function return values.
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type Data = Awaited<Promise<Promise<string>>>; // stringBuild a type-safe event map using template literals and mapped types.
type Events = { click: MouseEvent; keydown: KeyboardEvent }; type OnEvents = { [K in keyof Events as `on${Capitalize<K & string>}`]: (e: Events[K]) => void; }; // { onClick: (e: MouseEvent) => void; onKeydown: (e: KeyboardEvent) => void }Use conditional types to unwrap nested result types safely.
type UnwrapResult<T> = T extends { ok: true; value: infer V } ? V : T extends { ok: false; error: infer E } ? never : never;
FAQ
Use infer within an extends clause to capture a type variable: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never. This lets you pull out nested types like promise resolutions or function parameters without manually specifying them.
Mapped types iterate over keys to transform an existing type's structure (e.g., making all properties optional), while conditional types act like type-level ternaries to choose between types based on a condition. You often combine them — a mapped type whose values use a conditional type — for powerful transformations.
Yes — define a type like type EventName = on${Capitalize
TypeScript limits recursive type instantiation depth to prevent infinite loops; this typically happens with unbounded recursive conditional types. Break the recursion with a depth counter tuple or split the type into smaller, composable helper types to stay within the limit.
Combine mapped types with conditional types: type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>. This preserves optional fields outside K while enforcing presence for the keys you specify.