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.
| Concept | Syntax | Example use |
|---|---|---|
keyof | keyof T | Union of object keys |
| Indexed access | T[K] | Property type lookup |
| Mapped | { [K in keyof T]: ... } | Partial, Readonly |
| Conditional | T extends U ? A : B | Branch on assignability |
infer | Inside conditional | Extract 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"];
// stringReal-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]; // numberInterview:
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>; // falseDistributive 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 : Ybehaves differently forstring | numbervs 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>; // stringExtract 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 nullPick 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 layersSenior Interview Summary
What to say
keyofand indexed access derive types from existing shapes. Mapped types loop over keys; conditional types branch on assignability;inferextracts types from matched positions. Together they implementPartial,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 firstPractical 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