⚡IntermediateAdvanced Type System
Advanced Generics
Master generic constraints, default type parameters, multiple type variables, and higher-order generic patterns.
Overview
Generics go far beyond simple `<T>` placeholders. At the intermediate level you use **constraints** to restrict what a type parameter can be, **default types** to make generics optional, and **multiple type parameters** that relate to each other.
**Constraints** use `extends` to say "T must be at least this shape". `<T extends object>` rules out primitives; `<T extends { id: number }>` requires an id field. TypeScript then lets you access those guaranteed properties inside the generic.
**Default type parameters** (`<T = string>`) let callers omit the type argument when the default is fine - exactly like default function parameters.
**Bounded generics** combine both: `<T extends Record<string, unknown> = Record<string, unknown>>`. Higher-order generics accept other generics as parameters, enabling patterns like factory functions and builder APIs that remain fully type-safe.
Key Points
- •Constraint syntax: `<T extends SomeType>` - T must be assignable to SomeType
- •Access constrained properties inside the function: if `T extends { name: string }`, you can read `value.name`
- •Default type parameters: `<T = string>` - used when caller omits the type argument
- •Multiple related parameters: `<K extends keyof T, T>` - K must be a key of T
- •`keyof` + generics: `function get<T, K extends keyof T>(obj: T, key: K): T[K]` is fully type-safe property access
- •Generic constraints can reference other type params: `<A, B extends A>` means B must extend A
- •Generic interfaces and classes: constraints apply at the class/interface level, not just functions
Examples
Generic constraints - requiring shape
// Without constraint, T[key] errors because T could be anything
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', active: true };
const name = getProperty(user, 'name'); // string
const id = getProperty(user, 'id'); // number
// getProperty(user, 'missing'); // Error: not a key of typeof user
// Constraint requiring a specific shape
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const found = findById(users, 1); // { id: number; name: string } | undefinedDefault type parameters
// Default lets callers omit the type argument
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
// Explicit type argument
const userResponse: ApiResponse<{ id: number; name: string }> = {
data: { id: 1, name: 'Alice' },
status: 200,
message: 'OK',
};
// No type argument - defaults to unknown
const raw: ApiResponse = {
data: { anything: true },
status: 200,
message: 'OK',
};
// Generic class with default
class Repository<T extends { id: number } = { id: number }> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
findById(id: number): T | undefined {
return this.items.find(i => i.id === id);
}
}
const repo = new Repository<{ id: number; name: string }>();
repo.add({ id: 1, name: 'Alice' });
console.log(repo.findById(1));Multiple related type parameters
// K must be a key of T - the return type T[K] is inferred precisely
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const full = { id: 1, name: 'Alice', email: 'a@a.com', age: 30 };
const partial = pick(full, ['id', 'name']); // { id: number; name: string }
// Factory function - generic over the created type
function createPair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const pair = createPair('hello', 42); // [string, number]
// Merge two objects with type safety
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
const merged = merge({ name: 'Alice' }, { age: 30 });
// { name: string; age: number }
console.log(merged.name, merged.age);Common Mistakes
- Using `any` instead of a constraint: `<T extends object>` is far safer than `<T extends any>` - the former rejects primitives
- Putting the constraint on the wrong parameter: `<K, T extends K>` and `<T, K extends keyof T>` are very different; order matters
- Forgetting that a constrained T is still generic: inside a function, T is the specific type the caller passed, not the constraint itself
Note
Think of `T extends Constraint` as a minimum requirement contract: "I promise T will have at least these properties." The caller passes a more specific type, TypeScript narrows T to that specific type - not to the constraint floor.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 4 exercises
Knowledge Check
Question 1 of 4