TypeScript Generic Utility Patterns
TypeScript ships with utility types like Partial, Pick, and Record, but real projects constantly need transformations those built-ins don’t cover. Generic utility patterns are the techniques for building your own: combining generic type parameters with mapped types, conditional types, and the infer keyword to write reusable, type-safe transformations. Once you understand these patterns, you can read (and write) almost any type definition in a modern TypeScript codebase, including the source of lib.es5.d.ts itself.
Overview: How Generic Utility Types Work
A generic utility type is just a type alias with one or more type parameters that transforms its input into a new shape. The three building blocks you combine to do this are:
- Mapped types —
{ [K in keyof T]: ... }iterates over every key ofTand produces a new object type, optionally changing each property’s modifiers (?,readonly) or renaming keys with anasclause. - Conditional types —
T extends U ? X : Ylets a type branch based on a type-level check, similar to a ternary but evaluated by the compiler instead of at runtime. infer— used inside theextendsclause of a conditional type to “capture” a sub-part of a type so you can reuse it in the true branch, e.g. pulling the resolved value out of aPromise.
A crucial subtlety: when a conditional type’s checked type parameter is a bare, naked type parameter (not wrapped in an array or object), and you pass it a union type, TypeScript distributes the conditional over each member of the union separately, then unions the results back together. This is called a distributive conditional type, and it’s why Exclude<T, U> works correctly on unions without you writing a loop.
It’s also essential to remember that all of this happens purely at compile time. The type checker uses these definitions to validate your code and then erases every type from the emitted JavaScript. A recursive mapped type that looks intimidating in your editor produces zero runtime code — the compiled output is exactly the JavaScript you would have written by hand, with no trace of DeepPartial, infer, or any other type-level construct.
Syntax
The general shape of a generic utility type combines a type parameter list, an optional default, and a mapped or conditional body:
type Utility<T, U = unknown> = {
[K in keyof T]: T[K] extends U ? T[K] : never;
};
T— the primary type parameter being transformed; the caller supplies this, e.g.Utility<Product>.U = unknown— a second type parameter with a default, so callers can omit it:Utility<Product>falls back tounknown.[K in keyof T]— a mapped type clause;Kranges over every property key ofT.T[K] extends U ? T[K] : never— a conditional type evaluated once per key, keeping the property’s type if it matchesUand replacing it withneverotherwise.
Examples
Example 1: A recursive DeepPartial<T>
The built-in Partial<T> only makes top-level properties optional. A recursive generic utility can make every nested property optional too, which is exactly what you want for a “patch” argument to an update function.
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Address {
street: string;
city: string;
zip: string;
}
interface User {
id: number;
name: string;
address: Address;
}
function updateUser(user: User, patch: DeepPartial<User>): User {
return {
...user,
...patch,
address: { ...user.address, ...patch.address },
};
}
const original: User = {
id: 1,
name: "Ada",
address: { street: "1 Main St", city: "London", zip: "SW1" },
};
const updated = updateUser(original, { address: { city: "Manchester" } });
console.log(updated.name, updated.address.city, updated.address.street);
Output:
Ada Manchester 1 Main St
DeepPartial<T> checks whether T is an object; if so it maps over its keys, marks each optional with ?, and recurses into DeepPartial<T[K]> for nested objects like Address. Primitive properties (like string) hit the : T branch and are returned unchanged. This lets updateUser accept a partial, partially-nested patch object without you having to hand-write a second interface.
Example 2: Filtering keys by value type with PickByType<T, V>
Key remapping (the as clause inside a mapped type, added in TypeScript 4.1) lets you build a utility that keeps only the properties whose value type matches a given condition — useful for things like extracting all numeric fields of a model.
type PickByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface Product {
id: number;
title: string;
price: number;
inStock: boolean;
tags: string[];
}
type NumericFields = PickByType<Product, number>;
const stock: NumericFields = { id: 42, price: 19.99 };
console.log(JSON.stringify(stock));
Output:
{"id":42,"price":19.99}
For every key K, the as clause computes a new key name: either K itself (if T[K] matches ValueType) or never. Mapping a property to key type never is TypeScript’s way of saying “drop this property entirely” from the resulting mapped type. The result, NumericFields, only contains id and price — exactly the keys of Product whose value type is number.
Example 3: A generic Result<T, E> with a generic transform function
Generic utility patterns aren’t limited to type aliases — generic functions that operate over a generic type are just as important. Here’s a small, reusable “result” pattern for representing success or failure without exceptions, plus a generic mapResult helper that transforms the success value while leaving errors untouched.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
function mapResult<T, U, E>(
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> {
return result.ok ? ok(fn(result.value)) : result;
}
function parsePositiveNumber(input: string): Result<number, string> {
const parsed = Number(input);
if (Number.isNaN(parsed) || parsed <= 0) {
return err(`"${input}" is not a positive number`);
}
return ok(parsed);
}
const doubled = mapResult(parsePositiveNumber("21"), (n) => n * 2);
if (doubled.ok) {
console.log(`Doubled value: ${doubled.value}`);
} else {
console.log(`Error: ${doubled.error}`);
}
Output:
Doubled value: 42
Result<T, E = Error> is a generic discriminated union with a default type parameter, so callers can write Result<number> when the error type is just Error. The never return types on ok and err let each helper produce a Result that’s freely assignable into any more specific Result<T, E>, because never is assignable to every type. mapResult is itself generic over three parameters (T, U, E) and only calls fn when the result narrows to the success branch.
Under the Hood: What the Compiler Actually Does
When TypeScript encounters DeepPartial<User>, it does not run any code — it performs type-level substitution and evaluation, similar to how a macro system might expand a template:
- It substitutes
T = Userinto the conditionalT extends object ? ... : T. SinceUseris an object type, it takes the true branch. - It evaluates the mapped type
{ [K in keyof T]?: DeepPartial<T[K]> }, iterating overUser‘s keys (id,name,address) and recursively instantiatingDeepPartialfor each property’s type. - For
address: Address, it recurses:Address extends objectis true, so it maps overAddress‘s keys too, producing an optional, nested shape. - For
id: number, the recursion bottoms out at the: Tbranch becausenumberis not an object, so it staysnumber(just made optional by the outer mapped type).
The distributive behavior of conditional types follows a similar substitution rule, but with an extra step for unions: writing T extends U ? X : Y where T is a naked type parameter causes TypeScript to distribute over each union member of whatever is passed in for T. So instantiating with T = A | B effectively evaluates to (A extends U ? X : Y) | (B extends U ? X : Y). Once the compiler finishes checking your file, every one of these types disappears — the emitted JavaScript for the examples above contains only the interface-free runtime code: plain object literals, function calls, and console.log.
Common Mistakes
Mistake 1: Indexing with an unconstrained generic key
It’s tempting to write a generic “get property” helper with two independent type parameters, but without telling TypeScript that the key actually belongs to the object, indexing fails:
function pluck<T, K>(obj: T, key: K) {
return obj[key];
}
This fails under --strict with an error along the lines of: Element implicitly has an 'any' type because expression of type 'K' can't be used to index type 'T'. TypeScript has no way to know that every possible K is actually a valid key of every possible T, so it refuses to allow the index access. The fix is to constrain K to keyof T, which also lets TypeScript infer the precise return type:
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const point = { x: 10, y: 20 };
console.log(pluck(point, "x"));
Output:
10
Mistake 2: Leaving an error type parameter unconstrained
A generic ApiResponse<T, E> pattern often needs its error type used somewhere that expects a specific shape, such as the string constructor argument of Error. Forgetting to constrain E lets it compile at the type-alias level but fail at the call site:
type ApiResponse<T, E = string> = { data: T } | { error: E };
function unwrap<T, E>(response: ApiResponse<T, E>): T {
if ("data" in response) {
return response.data;
}
throw new Error(response.error);
}
This produces an error similar to: Argument of type 'E' is not assignable to parameter of type 'string'. Because E has no constraint, TypeScript only knows it could be anything, so it can’t guarantee response.error is a valid Error message. Constraining E (and giving it a matching default) fixes it:
type ApiResponse<T, E = string> = { data: T } | { error: E };
function unwrap<T, E extends string = string>(
response: ApiResponse<T, E>
): T {
if ("data" in response) {
return response.data;
}
throw new Error(response.error);
}
const good: ApiResponse<number> = { data: 100 };
console.log(unwrap(good));
Output:
100
Best Practices
- Constrain every generic type parameter as tightly as the utility actually needs (
K extends keyof T,E extends string, etc.) instead of leaving it open and reaching foranylater. - Give secondary type parameters sensible defaults (
E = Error) so common call sites stay short, while still allowing full customization when needed. - When a conditional type must NOT distribute over unions, wrap both sides in a tuple:
[T] extends [U] ? X : Y. - Prefer composing existing utility types (
Partial,Pick,Record,ReturnType) inside your own generic utilities rather than reinventing them from scratch. - Name utility types by what they produce, not how they’re implemented (
DeepPartial,PickByType), so callers can use them without reading the definition. - Test a new utility type against a handful of concrete types with
type Check = ExpectedShape extends ActualShape ? true : falsestyle checks, since type-level logic has no unit-test runner of its own. - Remember types vanish at compile time — never rely on a generic parameter to influence runtime behavior; pass an explicit runtime value (like a class constructor or a string tag) if you need that.
Practice Exercises
- Exercise 1: Write a generic utility type
ReadonlyDeep<T>that recursively makes every property of an object typereadonly, similar to howDeepPartialrecursively adds?. Test it against an interface with a nested object property. - Exercise 2: Write a generic function
firstOrDefault<T>(items: T[], fallback: T): Tthat returns the first element of an array or a fallback value if the array is empty. Then write a second version constrained withT extends {}sonullandundefinedcan’t be passed as the fallback. - Exercise 3: Using key remapping, write
PickByType<T, V>‘s opposite:OmitByType<T, V>, which keeps only the properties whose value type does not matchV. Test it on theProductinterface from Example 2, expecting it to keeptitle,inStock, andtags.
Summary
- Generic utility patterns combine type parameters with mapped types, conditional types, and
inferto build reusable, custom type transformations. - Mapped types (
{ [K in keyof T]: ... }) iterate over an object’s keys; adding anasclause lets you rename or drop keys conditionally. - Conditional types (
T extends U ? X : Y) branch at the type level and distribute automatically over unions whenTis a bare type parameter. - Constrain generic parameters (
K extends keyof T) instead of leaving them open — unconstrained parameters cause indexing and argument-assignability errors. - All of this is compile-time only: types are fully erased, so the emitted JavaScript never contains generics, conditional types, or mapped types.
- Give secondary type parameters defaults to keep common usage short while remaining fully customizable.
