Back to Blog
TypeScriptGenericsConstraintsType System

TypeScript Generic Constraints

Master generic constraints in TypeScript. From extends to keyof to conditional constraints.

B
Bootspring Team
Engineering
November 29, 2020
7 min read

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 name

keyof 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.

Share this article

Help spread the word about Bootspring