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, individuallytype 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 droppedRemapping 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">; // truetype B = IsString<42>; // falseT 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 arraytype ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // stringtype B = ElementType<number>; // number โ not an array, so T itselfThe 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> worksinfer 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> workstype Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // stringtype B = Unwrap<number>; // number โ not a Promise, so unchanged// Extracting the first argument of a functiontype FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
function handleEvent(event: MouseEvent, context: string) { ... }type EventArg = FirstArg<typeof handleEvent>; // MouseEventDistributive 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 parametertype ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Combined = ToArrayNonDistributive<string | number>; // (string | number)[] โ distribution suppressedWrapping 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
| Construct | Purpose | Example |
|---|---|---|
{ [K in keyof T]: ... } | Transform every property of T | Custom Partial, Readonly, etc. |
[K in keyof T as NewKey]: ... | Transform property names, or drop some entirely | Prefixed getters, filtering by value type |
T extends U ? X : Y | Type-level conditional | Branching logic based on a type shape |
infer R inside a conditional | Extract a piece of a matched type | ReturnType, Awaited, element-type extraction |
[T] extends [U] ? X : Y | Suppress distribution over unions | Forcing a union to be treated as one combined type |
Combining Mapped and Conditional Types
// Only make properties optional where the value type already includes undefinedtype 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.