1// Generic function example2functionidentity<T>(value:T):T{3return value;4}56// Usage with different types7const stringValue =identity<string>('hello');// string8const numberValue =identity<number>(42);// number9const inferred =identity('world');// TypeScript infers string or whatever type you pass in
This example demonstrates the core concept of generics: creating reusable code that works with multiple types while maintaining type safety. The <T> syntax defines a type parameter that gets replaced with the actual type when the function is called.
What are Generics?
Generics are TypeScript's way of creating reusable components that work with multiple types rather than a single type.
Think of them as type variables; placeholders that get filled in with specific types.
The Problem Generics Solve:
Without generics, you'd need to create separate functions for each type:
1// Without generics - repetitive code2functionidentityString(value:string):string{3return value;4}56functionidentityNumber(value:number):number{7return value;8}910// With generics - one function for all types11functionidentity<T>(value:T):T{12return value;13}
Key Benefits:
Type Safety: Maintain type checking while working with different types
Code Reusability: Write once, use with multiple types
Better IntelliSense: Get accurate autocomplete and type hints
No Runtime Overhead: Generics are purely compile-time constructs
Basic Generic Syntax
Generic Functions
The most common use of generics is in functions:
1functiongetFirst<T>(items:T[]):T|undefined{2return items[0];3}45// Usage6const firstNumber =getFirst<number>([1,2,3]);// number | undefined7const firstString =getFirst<string>(['a','b','c']);// string | undefined89// TypeScript can infer the type10const inferred =getFirst([true,false]);// boolean | undefined
Type Inference: TypeScript can often infer the generic type from the arguments, so you don't always need to explicitly specify it.
1classStack<T>{2private items:T[]=[];34push(item:T):void{5this.items.push(item);6}78pop():T|undefined{9returnthis.items.pop();10}11}1213// Usage14const numberStack =newStack<number>();15numberStack.push(1);16numberStack.push(2);17const top = numberStack.pop();// number | undefined
Generic Constraints
Constraints allow you to limit what types can be used with a generic:
1// Constraint: T must have a length property2functiongetLength<Textends{ length:number}>(item:T):number{3return item.length;4}56// Works with arrays, strings, etc.7getLength([1,2,3]);// 38getLength('hello');// 59// getLength(42); // Error: number doesn't have length
Using keyof with Constraints
A powerful pattern is constraining to object keys:
Why This Works:K extends keyof T ensures that key must be a valid property name of T. TypeScript then knows the return type is T[K], which is the type of that specific property.
Common Use Cases
Array Utilities
1// Get last element2functiongetLast<T>(items:T[]):T|undefined{3return items[items.length -1];4}56// Filter by property7functionfilterBy<T,KextendskeyofT>(8 items:T[],9 key:K,10 value:T[K]11):T[]{12return items.filter((item)=> item[key]=== value);13}1415// Usage16interfacePerson{17 name:string;18 age:number;19}2021const people: Person[]=[22{ name:'Alice', age:30},23{ name:'Bob', age:25},24];2526const adults =filterBy(people,'age',30);// Person[]
API Response Wrapper
1interfaceApiResponse<T>{2 data:T;3 status:number;4 message?:string;5}67asyncfunctionfetchData<T>(url:string):Promise<ApiResponse<T>>{8const response =awaitfetch(url);9const data =await response.json();10return{11 data,12 status: response.status,13};14}1516// Usage17interfaceUser{18 id:string;19 name:string;20}2122const userResponse =awaitfetchData<User>('/api/user');23// userResponse.data is typed as User
Why This Pattern Works: This ensures that when you fetch different endpoints, the data property is correctly typed. Fetching /api/user gives you ApiResponse<User>, while /api/products could give you ApiResponse<Product[]>.
Best Practices
Leverage Type Inference
1// TypeScript can infer the type2const result =identity('hello');// string34// Only specify when necessary5const result2 =identity<string|null>(null);// string | null
Use Constraints Judiciously
1// Good: Constraint enables functionality2functiongetLength<Textends{ length:number}>(item:T):number{3return item.length;4}56// Avoid: Unnecessary constraint limits flexibility7functionprocess<Textendsstring>(value:T):T{8return value;9}10// Better: Let it work with any type11functionprocess<T>(value:T):T{12return value;13}
Generics are a fundamental feature of TypeScript that enable you to write reusable, type-safe code. They allow you to create components that work with multiple types while maintaining full type safety and IntelliSense support.
Key Takeaways:
Generics create reusable code that works with multiple types
Type inference often eliminates the need to explicitly specify types
Constraints allow you to limit and enable specific functionality
Generics work with functions, interfaces, classes, and type aliases
Remember: Generics are about creating flexible, reusable code while maintaining type safety. Start simple and add complexity only when needed.
I build fast, accessible, and delightful digital experiences for the web. Whether you have a project in mind or just want to connect, I’d love to hear from you.