Technology  /  TypeScript

๐Ÿ”ท TypeScript 16 guides ยท updated 2026

Type erasure, structural typing, generics, and the type-level programming toolkit โ€” TypeScript taught the way it actually behaves at compile time and at runtime.

Mapped and Conditional Types in TypeScript: Building Your Own Utility Types

Partial<T>, Pick<T, K>, and every other utility type from the previous guide arenโ€™t compiler magic โ€” theyโ€™re ordinary TypeScript, written using two constructs available to you too: mapped types (transform every property of an existing type) and conditional types (choose a type based on a type-level condition). Once you can read and write both, you stop being limited to whatever utility types the standard library happened to ship, and start building the exact transformation your specific problem needs.


Mapped Types โ€” Transforming Every Property at Once

type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: string;
name: string;
}
type PartialUser = MyPartial<User>; // { id?: string; name?: string } โ€” this IS how Partial<T> is defined

[K in keyof T] reads as โ€œfor every key K in the keys of Tโ€ โ€” it iterates over Tโ€™s property names the way a for...in loop iterates over an objectโ€™s keys at runtime, except this happens entirely at the type level, at compile time. T[K] is an indexed access type: โ€œthe type of property K on T.โ€ Adding ? after K in keyof T makes each resulting property optional โ€” this three-line definition is, almost verbatim, how TypeScriptโ€™s actual Partial<T> is implemented in its standard library .d.ts file.

// The building blocks, individually
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? removes optionality (the opposite of adding it)
};
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type MyMutable<T> = {
-readonly [K in keyof T]: T[K]; // -readonly strips readonly โ€” the opposite of adding it
};

The - prefix on a modifier (-?, -readonly) explicitly removes that modifier rather than adding it โ€” necessary because without it, mapping over an already-optional or already-readonly type would just preserve those modifiers rather than stripping them, and there needed to be a way to express โ€œmake this required/mutable regardless of what it started as.โ€


Key Remapping With as

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

The as clause inside a mapped type lets you transform the key itself, not just the value โ€” here, every property name K becomes get${Capitalize<K>}, turning name into getName and age into getAge, using a template literal type (covered in depth in the next guide) to construct the new key. This pattern is exactly how libraries auto-generate typed getter/setter method names, or derive event-handler prop names (onClick from click) from a base set of properties.

// Filtering keys out entirely during remapping, using `never`
type OmitByValue<T, V> = {
[K in keyof T as T[K] extends V ? never : K]: T[K];
};
interface Config {
name: string;
retries: number;
timeout: number;
}
type OnlyStrings = OmitByValue<Config, number>; // { name: string } โ€” number-valued keys dropped

Remapping a key to never causes TypeScript to drop that property entirely from the resulting mapped type โ€” this is the mechanism as-based key filtering relies on, and itโ€™s a deliberate, well-defined behavior rather than an edge case.


Conditional Types โ€” Type-Level if/else

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

T extends U ? X : Y reads as โ€œif T is assignable to U, the result is X; otherwise, itโ€™s Yโ€ โ€” a genuine conditional, evaluated entirely at compile time, that lets a typeโ€™s shape depend on another type rather than being fixed in advance.

// A practical conditional type: extract the element type of an array, or the type itself if it's not an array
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number โ€” not an array, so T itself

The infer Keyword โ€” Extracting a Type From Within Another Type

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: "1", name: "Alice" };
}
type User = ReturnTypeOf<typeof getUser>; // { id: string; name: string } โ€” this IS how ReturnType<T> works

infer R inside a conditional type declares a new type variable that TypeScript fills in by pattern-matching against the actual type being checked โ€” here, โ€œif T is some function type, capture whatever its return type is and call it R.โ€ This is the single most powerful primitive in TypeScriptโ€™s type-level programming toolkit: it lets you pull a piece out of a larger type shape without knowing that piece in advance.

// Extracting a Promise's resolved value type โ€” this IS roughly how Awaited<T> works
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<number>; // number โ€” not a Promise, so unchanged
// Extracting the first argument of a function
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
function handleEvent(event: MouseEvent, context: string) { ... }
type EventArg = FirstArg<typeof handleEvent>; // MouseEvent

matches

doesn't match

type T

T extends SomePattern<infer X> ?

X is bound to whatever

matched inside the pattern

Falls through to the

else branch of the conditional


Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never;
type StringOrNumberArray = ToArray<string | number>;
// You might expect (string | number)[] โ€” but you actually get string[] | number[]

This is one of the most surprising behaviors in the whole type system: when a conditional typeโ€™s checked type (T here) is a naked type parameter and the type passed in is a union, TypeScript distributes the conditional over each union member separately, then unions the results back together โ€” ToArray<string | number> doesnโ€™t produce one array type containing both, it produces a union of two separate array types. This is usually exactly what you want (itโ€™s how Exclude<T, U> and Extract<T, U> from the utility types guide actually work), but it surprises people the first time they expect a single combined result instead.

// Suppressing distribution โ€” wrap T in a tuple to make it a "non-naked" type parameter
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Combined = ToArrayNonDistributive<string | number>; // (string | number)[] โ€” distribution suppressed

Wrapping T in a one-element tuple ([T]) on both sides of extends is the standard trick for opting out of distribution when you genuinely want the union treated as one combined type rather than processed member-by-member.


A Realistic Custom Utility Type

// Deep partial โ€” recursively makes nested objects optional too, unlike the built-in shallow Partial<T>
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Settings {
user: {
name: string;
preferences: {
theme: string;
notifications: boolean;
};
};
}
type PartialSettings = DeepPartial<Settings>;
// Every level is optional now, including nested preferences โ€” built-in Partial<T> only reaches one level deep
function updateSettings(updates: PartialSettings) { ... }
updateSettings({ user: { preferences: { theme: "dark" } } }); // valid โ€” deeply partial, unlike Partial<Settings>

This is the exact gap the utility types guide flagged: the built-in Partial<T> only makes the top level of properties optional โ€” nested object properties are still fully required inside a Partial<Settings>. DeepPartial<T> fixes that with a recursive conditional type: if T[K] is itself an object, recurse into it with DeepPartial again; otherwise, leave the primitive value as-is.


Conditional Types With Multiple Branches

type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type A = TypeName<string>; // "string"
type B = TypeName<() => void>; // "function"
type C = TypeName<{ x: number }>; // "object"

Chained conditional types (each false branch containing another conditional) are TypeScriptโ€™s equivalent of an if/else if/else if/else chain โ€” evaluated top to bottom, first match wins, entirely at compile time.


Mapped and Conditional Types Quick Reference

ConstructPurposeExample
{ [K in keyof T]: ... }Transform every property of TCustom Partial, Readonly, etc.
[K in keyof T as NewKey]: ...Transform property names, or drop some entirelyPrefixed getters, filtering by value type
T extends U ? X : YType-level conditionalBranching logic based on a type shape
infer R inside a conditionalExtract a piece of a matched typeReturnType, Awaited, element-type extraction
[T] extends [U] ? X : YSuppress distribution over unionsForcing a union to be treated as one combined type

Combining Mapped and Conditional Types

// Only make properties optional where the value type already includes undefined
type OptionalUndefinedProps<T> = {
[K in keyof T as undefined extends T[K] ? K : never]?: T[K];
} & {
[K in keyof T as undefined extends T[K] ? never : K]: T[K];
};
interface Form {
name: string;
nickname: string | undefined;
}
type FormFields = OptionalUndefinedProps<Form>;
// { nickname?: string | undefined } & { name: string }

Mapped types and conditional types are frequently combined exactly like this: the conditional (undefined extends T[K] ? K : never) decides which keys survive the as remap, and the surrounding mapped type applies the transformation to whichever keys make it through. Reading nested mapped/conditional types like this gets easier with practice โ€” work from the innermost condition outward, one extends check at a time.

Frequently Asked Questions

Do I need to write custom mapped/conditional types often in normal application code? Rarely for typical CRUD application code โ€” the built-in utility types cover most everyday needs. They matter most when writing shared library code, generic form/validation helpers, or ORM-style typed query builders, where the shape of the output genuinely depends on the shape of the input in a way no built-in utility expresses.

Why did my conditional type produce a union I didnโ€™t expect? Almost certainly distributive conditional types โ€” check whether the type parameter being tested is a โ€œnakedโ€ one (used directly in the extends clause) rather than wrapped in something like a tuple. Wrapping in [T] extends [U] suppresses distribution if itโ€™s not what you wanted.

Is infer limited to function return types? No โ€” infer can appear inside any structural pattern: array element types, Promise resolved types, tuple positions, even nested inside other conditional types. Anywhere you can write a type โ€œshapeโ€ with a placeholder, infer can capture what fills that placeholder.

Can mapped types change a propertyโ€™s type, not just its modifiers? Yes โ€” [K in keyof T]: SomeTransform<T[K]> can wrap every propertyโ€™s value type in another type constructor (like the DeepPartial example, or a hypothetical Promisify<T> that wraps every property in Promise<...>), not just add or remove readonly/?.

Are these constructs slow to type-check? Deeply recursive conditional/mapped types (like unrestricted DeepPartial over very large or self-referential types) can noticeably slow down compilation in extreme cases, and TypeScript has a built-in recursion depth limit to prevent infinite loops. For everyday application-sized types, this is not a practical concern.


Whatโ€™s Next

You can now read (and write) the actual definitions behind TypeScriptโ€™s built-in utility types, and build your own when the built-ins donโ€™t fit. Next: Template Literal Types, which covers the string-pattern type-level programming this guide touched on briefly with Getters<T> โ€” plus keyof and typeof in more depth than weโ€™ve needed so far.