Topics
v5.0.0
On this page · sections

Mapped & Conditional Types

Mapped types transform every key in an object. Conditional types pick one branch based on assignability (T extends U ? A : B). Together with keyof, indexed access, and infer, they power most advanced utilities in TypeScript.

ConceptSyntaxExample use
keyofkeyof TUnion of object keys
Indexed accessT[K]Property type lookup
Mapped{ [K in keyof T]: ... }Partial, Readonly
ConditionalT extends U ? A : BBranch on assignability
inferInside conditionalExtract inner type

keyof and indexed access

Definition

keyof T produces a union of T’s keys. Indexed access T[K] looks up the type at key K.

type User = { id: string; name: string; role: "admin" | "viewer"; }; type UserKeys = keyof User; // "id" | "name" | "role" type UserName = User["name"]; // string type UserFields = User["id" | "name"]; // string

Real-world example — typed pick

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { const result = {} as Pick<T, K>; for (const key of keys) { result[key] = obj[key]; } return result; } const preview = pick(user, ["id", "name"]); // { id: string; name: string }

Arrays and tuples

const ROLES = ["admin", "editor", "viewer"] as const; type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer" type Tuple = [string, number, boolean]; type Second = Tuple[1]; // number

Interview: keyof + indexed access is how you derive types from existing types instead of duplicating field lists.

Mapped types

Definition

Iterate keys of T and produce a new shape:

type Optional<T> = { [K in keyof T]?: T[K]; }; type User = { id: string; name: string }; type PartialUser = Optional<User>; // { id?: string; name?: string }

Key modifiers

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

-? removes optional; -readonly removes readonly. These modifiers are mapped-type specific.

Remap keys

type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; type UserGetters = Getters<{ name: string; age: number }>; // { getName: () => string; getAge: () => number }

Filter keys with as + conditional

type FunctionKeys<T> = { [K in keyof T as T[K] extends Function ? K : never]: T[K]; };

Conditional types

Definition

type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false

Distributive behavior

When T is a naked type parameter, conditionals distribute over unions:

type ToArray<T> = T extends unknown ? T[] : never; type R = ToArray<string | number>; // string[] | number[]

Wrap in a tuple to disable distribution:

type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never; type R2 = ToArrayNonDist<string | number>; // (string | number)[]

Interview: Distribution is why T extends U ? X : Y behaves differently for string | number vs a single type.

The infer keyword

Definition

infer declares a type variable inside a conditional type, inferred from the matched position.

type ElementType<T> = T extends (infer E)[] ? E : T; type N = ElementType<number[]>; // number type S = ElementType<string>; // string

Extract function return type (how ReturnType works)

type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never; type Fn = () => { id: string }; type R = MyReturnType<Fn>; // { id: string }

Extract function parameters

type MyParameters<T> = T extends (...args: infer P) => unknown ? P : never; type P = MyParameters<(a: string, b: number) => void>; // [a: string, b: number]

Unwrap Promise

type AwaitedInner<T> = T extends Promise<infer U> ? U : T; type Data = AwaitedInner<Promise<{ items: string[] }>>; // { items: string[] }

Combining mapped + conditional

DeepReadonly — real-world utility

type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T; type Config = { api: { url: string; timeout: number }; flags: { beta: boolean }; }; type Frozen = DeepReadonly<Config>; // config.api.url = "x" // ❌

Functions are left unchanged — a common pattern in utility libraries.

Nullable field transforms

type NonNullableFields<T> = { [K in keyof T]: NonNullable<T[K]>; }; type NullableKeys<T> = { [K in keyof T]: null extends T[K] ? K : never; }[keyof T]; // Keys of T whose values include null

Pick by modifier

type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never; }[keyof T];

API DTO mapping pattern

type ApiUser = { id: string; full_name: string; created_at: string; }; type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never; }[keyof T]; type StringKeys = KeysMatching<ApiUser, string>; // "id" | "full_name" | "created_at"

Mapped types shine when transforming API payloads to client models at the type level (often paired with runtime mappers or code generation).

Built-in utilities (preview)

Most built-ins are mapped and/or conditional under the hood:

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

See the dedicated Utility Types guide for everyday usage patterns.

Limitations and performance

Deep recursive mapped types → can hit recursion depth limits Very wide keyof unions → slow IDE / tsserver Over-mapping in feature code → prefer built-ins in app layers

Senior Interview Summary

What to say

keyof and indexed access derive types from existing shapes. Mapped types loop over keys; conditional types branch on assignability; infer extracts types from matched positions. Together they implement Partial, ReturnType, and custom domain utilities.

Mental model

keyof T → "all key names" T[K] → "type at key K" [K in keyof T] → "loop every key" T extends U ? A : B → "if T fits U, use A else B" infer R → "capture type at this position"

When interviewers expect you to use them

Custom utility types → mapped + conditional Extract from functions → infer return/params Deep transformations → recursive conditional mapped Everyday app code → built-in utilities first

Practical rule

Reach for built-ins (Partial, Pick, ReturnType) before custom mapped types Use infer in library/shared types, not scattered in feature components Document distributive union behavior when writing generic conditionals Disable distribution with [T] extends [U] when you need a single branch

Last updated on