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 of T and produces a new object type, optionally changing each property’s modifiers (?, readonly) or renaming keys with an as clause.
  • Conditional typesT extends U ? X : Y lets 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 the extends clause 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 a Promise.

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 to unknown.
  • [K in keyof T] — a mapped type clause; K ranges over every property key of T.
  • T[K] extends U ? T[K] : never — a conditional type evaluated once per key, keeping the property’s type if it matches U and replacing it with never otherwise.

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:

  1. It substitutes T = User into the conditional T extends object ? ... : T. Since User is an object type, it takes the true branch.
  2. It evaluates the mapped type { [K in keyof T]?: DeepPartial<T[K]> }, iterating over User‘s keys (id, name, address) and recursively instantiating DeepPartial for each property’s type.
  3. For address: Address, it recurses: Address extends object is true, so it maps over Address‘s keys too, producing an optional, nested shape.
  4. For id: number, the recursion bottoms out at the : T branch because number is not an object, so it stays number (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 for any later.
  • 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 : false style 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 type readonly, similar to how DeepPartial recursively adds ?. Test it against an interface with a nested object property.
  • Exercise 2: Write a generic function firstOrDefault<T>(items: T[], fallback: T): T that returns the first element of an array or a fallback value if the array is empty. Then write a second version constrained with T extends {} so null and undefined can’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 match V. Test it on the Product interface from Example 2, expecting it to keep title, inStock, and tags.

Summary

  • Generic utility patterns combine type parameters with mapped types, conditional types, and infer to build reusable, custom type transformations.
  • Mapped types ({ [K in keyof T]: ... }) iterate over an object’s keys; adding an as clause lets you rename or drop keys conditionally.
  • Conditional types (T extends U ? X : Y) branch at the type level and distribute automatically over unions when T is 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.