TypeScript Mapped & Conditional Types: Deep Dive
These are the building blocks that all built-in utility types are constructed from.
Mapped Types Fundamentals
A mapped type iterates over the keys of another type:
type User = { id: number; name: string; email: string }
// The shape of Partial<T> type MyPartial<T> = { [K in keyof T]?: T[K] } type PartialUser = MyPartial<User> // { id?: number; name?: string; email?: string }
// Readonly<T> type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
// Map over a union of string literals type Flags = 'debug' | 'verbose' | 'strict' type FlagConfig = { [K in Flags]: boolean } // { debug: boolean; verbose: boolean; strict: boolean }
Modifier Tokens (+ and -)
Add or remove the optional (?) and readonly modifiers:
// -? removes optionality (Required<T>)
type Required<T> = { [K in keyof T]-?: T[K] }
// -readonly removes readonly type Mutable<T> = { -readonly [K in keyof T]: T[K] }
type ReadonlyUser = Readonly<User> type MutableUser = Mutable<ReadonlyUser> // all readonly removed
Key Remapping with as (TypeScript 4.1+)
Rename keys in a mapped type using template literals:
type Getters<T> = {
[K in keyof T as get${Capitalize<string & K>}]: () => T[K]
}
type UserGetters = Getters<User> // { getId: () => number; getName: () => string; getEmail: () => string }
// Filter keys using never: type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] } type StringFields = OnlyStrings<{ id: number; name: string; active: boolean }> // { name: string }
Conditional Types
// Basic form: T extends U ? X : Y
type IsArray<T> = T extends any[] ? true : false
type A = IsArray<number[]> // true
type B = IsArray<string> // false
// NonNullable<T> type NonNullable<T> = T extends null | undefined ? never : T type C = NonNullable<string | null | undefined> // string
// Extract element type type ElementType<T> = T extends (infer E)[] ? E : T type D = ElementType<User[]> // User type E = ElementType<string> // string (no array — falls through)
Distributive Conditional Types
When T is a union, conditional types are distributed over each member:
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number> // string[] | number[] — distributed! // Not (string | number)[] — that would be non-distributive
// Prevent distribution with tuple wrapping: type ToArrayND<T> = [T] extends [any] ? T[] : never type Result2 = ToArrayND<string | number> // (string | number)[] — non-distributive
The infer Keyword
infer captures a type from within a conditional type:
// Extract the return type of a function
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = MyReturnType<() => string> // string type R2 = MyReturnType<(x: number) => User> // User type R3 = MyReturnType<string> // never (not a function)
// Extract first argument type type FirstArg<T> = T extends (first: infer A, ...rest: any[]) => any ? A : never type F = FirstArg<(x: string, y: number) => void> // string
// Unwrap a Promise type UnPromise<T> = T extends Promise<infer V> ? V : T type U = UnPromise<Promise<User[]>> // User[]
// Extract object value types type ValueOf<T> = T extends Record<string, infer V> ? V : never type V = ValueOf<{ a: number; b: string; c: boolean }> // number | string | boolean
Building Custom Utility Types
// DeepPartial — every nested object's properties are optional
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
// PickByValue — pick keys whose values match a type type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] } type StringFields = PickByValue<User & { active: boolean }, string> // { name: string; email: string }
// Merge two types (second overrides first) type Merge<A, B> = Omit<A, keyof B> & B
Practice TypeScript questions at [JSPrep Pro](/auth).