Generic constraints ensure type parameters meet specific requirements. Here's how to use them.
Basic Constraints#
1// Constraint with extends
2function getLength<T extends { length: number }>(item: T): number {
3 return item.length;
4}
5
6getLength('hello'); // OK
7getLength([1, 2, 3]); // OK
8getLength({ length: 10 }); // OK
9getLength(123); // Error: number doesn't have length
10
11// Multiple constraints with intersection
12interface HasId {
13 id: number;
14}
15
16interface HasName {
17 name: string;
18}
19
20function processEntity<T extends HasId & HasName>(entity: T): void {
21 console.log(entity.id, entity.name);
22}
23
24processEntity({ id: 1, name: 'Alice', extra: true }); // OK
25processEntity({ id: 1 }); // Error: missing namekeyof Constraint#
1// Access object properties safely
2function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
3 return obj[key];
4}
5
6const user = { name: 'Alice', age: 30 };
7
8getProperty(user, 'name'); // string
9getProperty(user, 'age'); // number
10getProperty(user, 'email'); // Error: 'email' not in keyof user
11
12// Set property
13function setProperty<T, K extends keyof T>(
14 obj: T,
15 key: K,
16 value: T[K]
17): void {
18 obj[key] = value;
19}
20
21setProperty(user, 'age', 31); // OK
22setProperty(user, 'age', '31'); // Error: string not assignable to number
23
24// Pick multiple keys
25function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
26 const result = {} as Pick<T, K>;
27 keys.forEach(key => {
28 result[key] = obj[key];
29 });
30 return result;
31}
32
33const picked = pick(user, ['name']);
34// { name: string }Constructor Constraints#
1// Ensure type is constructable
2interface Constructor<T> {
3 new (...args: any[]): T;
4}
5
6function createInstance<T>(ctor: Constructor<T>): T {
7 return new ctor();
8}
9
10class MyClass {
11 value = 42;
12}
13
14const instance = createInstance(MyClass);
15console.log(instance.value); // 42
16
17// With arguments
18interface ConstructorWithArgs<T, A extends any[]> {
19 new (...args: A): T;
20}
21
22function createWithArgs<T, A extends any[]>(
23 ctor: ConstructorWithArgs<T, A>,
24 ...args: A
25): T {
26 return new ctor(...args);
27}
28
29class Person {
30 constructor(public name: string, public age: number) {}
31}
32
33const person = createWithArgs(Person, 'Alice', 30);Recursive Constraints#
1// Type that references itself
2interface TreeNode<T> {
3 value: T;
4 children?: TreeNode<T>[];
5}
6
7function traverseTree<T>(node: TreeNode<T>, fn: (value: T) => void): void {
8 fn(node.value);
9 node.children?.forEach(child => traverseTree(child, fn));
10}
11
12// Self-referential constraint
13type JSONValue =
14 | string
15 | number
16 | boolean
17 | null
18 | JSONValue[]
19 | { [key: string]: JSONValue };
20
21function parseJSON<T extends JSONValue>(json: string): T {
22 return JSON.parse(json);
23}
24
25// Recursive type with constraint
26type DeepPartial<T> = T extends object
27 ? { [P in keyof T]?: DeepPartial<T[P]> }
28 : T;
29
30interface Config {
31 server: {
32 host: string;
33 port: number;
34 };
35 debug: boolean;
36}
37
38const partialConfig: DeepPartial<Config> = {
39 server: { host: 'localhost' },
40};Conditional Constraints#
1// Different constraints based on condition
2type StringOrNumber<T> = T extends string
3 ? string
4 : T extends number
5 ? number
6 : never;
7
8function process<T extends string | number>(
9 value: T
10): StringOrNumber<T> {
11 if (typeof value === 'string') {
12 return value.toUpperCase() as StringOrNumber<T>;
13 }
14 return (value * 2) as StringOrNumber<T>;
15}
16
17// Constraint based on another generic
18type PropType<T, K> = K extends keyof T ? T[K] : never;
19
20function getProp<T, K extends string>(
21 obj: T,
22 key: K
23): PropType<T, K> {
24 return (obj as any)[key];
25}
26
27// Filter types
28type FilterByType<T, U> = {
29 [K in keyof T as T[K] extends U ? K : never]: T[K];
30};
31
32interface Mixed {
33 id: number;
34 name: string;
35 active: boolean;
36 count: number;
37}
38
39type NumberProps = FilterByType<Mixed, number>;
40// { id: number; count: number }Default Type Parameters#
1// Default generic type
2interface Container<T = string> {
3 value: T;
4}
5
6const stringContainer: Container = { value: 'hello' };
7const numberContainer: Container<number> = { value: 42 };
8
9// Default with constraint
10function createArray<T extends object = Record<string, unknown>>(
11 items: T[]
12): T[] {
13 return [...items];
14}
15
16// Multiple defaults
17interface Response<D = unknown, E = Error> {
18 data?: D;
19 error?: E;
20}
21
22const response: Response = { data: { id: 1 } };
23const typedResponse: Response<User, ApiError> = { data: user };Function Overloads with Generics#
1// Overloaded function with constraints
2function parse<T extends string>(input: T): string[];
3function parse<T extends number>(input: T): number;
4function parse<T extends string | number>(input: T): string[] | number {
5 if (typeof input === 'string') {
6 return input.split(',');
7 }
8 return input * 2;
9}
10
11const strings = parse('a,b,c'); // string[]
12const doubled = parse(21); // number
13
14// Generic overload
15interface Fetcher {
16 <T extends object>(url: string): Promise<T>;
17 <T extends object>(url: string, options: RequestInit): Promise<T>;
18}
19
20const fetcher: Fetcher = async (url: string, options?: RequestInit) => {
21 const response = await fetch(url, options);
22 return response.json();
23};Mapped Type Constraints#
1// Constrain mapped type keys
2type Getters<T> = {
3 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
4};
5
6interface Person {
7 name: string;
8 age: number;
9}
10
11type PersonGetters = Getters<Person>;
12// { getName: () => string; getAge: () => number }
13
14// Filter keys by constraint
15type ReadonlyKeys<T> = {
16 [K in keyof T]-?: IfEquals<
17 { [Q in K]: T[K] },
18 { -readonly [Q in K]: T[K] },
19 never,
20 K
21 >;
22}[keyof T];
23
24type IfEquals<X, Y, A, B> =
25 (<T>() => T extends X ? 1 : 2) extends
26 (<T>() => T extends Y ? 1 : 2) ? A : B;
27
28// Extract method keys
29type MethodKeys<T> = {
30 [K in keyof T]: T[K] extends Function ? K : never;
31}[keyof T];
32
33interface Service {
34 name: string;
35 start(): void;
36 stop(): void;
37}
38
39type ServiceMethods = MethodKeys<Service>;
40// 'start' | 'stop'Variance and Constraints#
1// Covariant constraint (output position)
2interface Producer<out T> {
3 produce(): T;
4}
5
6// Contravariant constraint (input position)
7interface Consumer<in T> {
8 consume(value: T): void;
9}
10
11// Invariant (both positions)
12interface Processor<T> {
13 process(value: T): T;
14}
15
16// Practical example
17type EventHandler<E extends Event> = (event: E) => void;
18
19function addEventListener<E extends Event>(
20 type: string,
21 handler: EventHandler<E>
22): void {
23 // ...
24}
25
26addEventListener<MouseEvent>('click', (e) => {
27 console.log(e.clientX);
28});Complex Constraints#
1// Ensure object has specific structure
2type HasMethod<T, M extends string> = T extends { [K in M]: Function }
3 ? T
4 : never;
5
6function callMethod<T extends { [key: string]: any }, M extends string>(
7 obj: HasMethod<T, M>,
8 method: M
9): ReturnType<T[M]> {
10 return obj[method]();
11}
12
13const service = {
14 start: () => 'started',
15 stop: () => 'stopped',
16};
17
18callMethod(service, 'start'); // OK
19callMethod(service, 'run'); // Error
20
21// Ensure array of same type
22function merge<T extends any[]>(...arrays: T[]): T[number][] {
23 return arrays.flat();
24}
25
26const merged = merge([1, 2], [3, 4]); // number[]
27
28// Ensure promise unwrapping
29type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
30
31async function unwrap<T>(value: T | Promise<T>): Promise<Awaited<T>> {
32 return await value as Awaited<T>;
33}Best Practices#
Design:
✓ Use minimal constraints
✓ Prefer interfaces over type literals
✓ Document constraint requirements
✓ Use meaningful type parameter names
Patterns:
✓ keyof for property access
✓ extends for structural constraints
✓ Conditional types for flexibility
✓ Default parameters for convenience
Avoid:
✗ Over-constraining generics
✗ Complex nested constraints
✗ Circular type references
✗ any in constraints
Testing:
✓ Test with edge cases
✓ Verify error messages
✓ Check inference results
✓ Test with unknown types
Conclusion#
Generic constraints ensure type safety while maintaining flexibility. Use extends for structural requirements, keyof for property access, and conditional types for advanced logic. Keep constraints minimal and document complex requirements.