Master complex generic patterns and techniques.

Higher-Order Generics

Generics that work with other generics.

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<string>;          // string

// Recursive unwrapping
type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>
  : T;

type C = DeepUnwrapPromise<Promise<Promise<string>>>; // string

Generic Constraints with keyof

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

function setProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

const user = { name: "Alice", age: 25 };
getProperty(user, "name"); // string
setProperty(user, "age", 30); // OK
// setProperty(user, "age", "30"); // Error: type mismatch

Conditional Generic Types

type ApiResponse<T> = T extends string
  ? { message: T }
  : T extends number
  ? { code: T }
  : { data: T };

type A = ApiResponse<string>;  // { message: string }
type B = ApiResponse<number>;  // { code: number }
type C = ApiResponse<User>;    // { data: User }

Generic Function Overloads

function process<T extends string>(value: T): `Processed ${T}`;
function process<T extends number>(value: T): T;
function process<T extends string | number>(
  value: T
): `Processed ${T}` | T {
  if (typeof value === "string") {
    return `Processed ${value}` as any;
  }
  return value;
}

Variadic Tuple Types

Work with tuples of varying lengths (TypeScript 4.0+).

type Concat<T extends readonly any[], U extends readonly any[]> = [
  ...T,
  ...U
];

type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

function concat<T extends readonly any[], U extends readonly any[]>(
  arr1: T,
  arr2: U
): Concat<T, U> {
  return [...arr1, ...arr2] as any;
}

Generic Type Predicates

function isArrayOf<T>(
  arr: unknown,
  predicate: (item: unknown) => item is T
): arr is T[] {
  return Array.isArray(arr) && arr.every(predicate);
}

function isString(item: unknown): item is string {
  return typeof item === "string";
}

const arr: unknown = ["a", "b", "c"];
if (isArrayOf(arr, isString)) {
  // arr is string[]
  arr.forEach(s => s.toUpperCase());
}

Exercises

Exercise 1: Higher-Order Generics

// Create a type 'UnwrapArray' that:
// - Unwraps a single level of array
// - Create 'DeepUnwrapArray' that unwraps recursively

Exercise 2: Conditional Generics

// Create a generic type that:
// - If T is a function, return its return type
// - If T is a promise, return the resolved type
// - Otherwise, return T

Exercise 3: Variadic Tuples

// Create a function 'zip' that:
// - Takes two arrays
// - Returns a tuple of pairs
// - Use variadic tuple types

Exercise 4: Generic Constraints

// Create a function 'merge' that:
// - Takes two objects
// - Returns a merged object
// - Preserves types correctly

Exercise 5: Complex Generic

// Create a generic type 'Path' that:
// - Takes an object type
// - Returns all possible paths as string literals
// - Example: Path<{ a: { b: string } }> => "a" | "a.b"

Best Practices

  1. Use constraints effectively - Limit generic types appropriately
  2. Leverage type inference - Let TypeScript infer when possible
  3. Document complex generics - Add comments explaining constraints
  4. Test edge cases - Ensure generics work with various types
  5. Avoid over-engineering - Keep generics simple when possible

Common Mistakes

  1. Too many type parameters
// ❌ Too complex
function process<T, U, V, W>(a: T, b: U, c: V): W { }

// ✅ Simpler
function process<T>(a: T): Processed<T> { }
  1. Not using constraints
// ❌ Error-prone
function getLength<T>(item: T): number {
  return item.length; // Error
}

// ✅ With constraint
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}
  1. Circular dependencies in generics
// ❌ Might cause issues
type A<T> = B<T>;
type B<T> = A<T>;

Next Steps

Learn about Conditional Types (Deep Dive).