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
- Use constraints effectively - Limit generic types appropriately
- Leverage type inference - Let TypeScript infer when possible
- Document complex generics - Add comments explaining constraints
- Test edge cases - Ensure generics work with various types
- Avoid over-engineering - Keep generics simple when possible
Common Mistakes
- 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> { }
- 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;
}
- 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).