Skip to main content
MSH Logo

Understanding TypeScript Generics

Published on
1// Generic function example 2function identity<T>(value: T): T { 3 return value; 4} 5 6// Usage with different types 7const stringValue = identity<string>('hello'); // string 8const numberValue = identity<number>(42); // number 9const 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 code 2function identityString(value: string): string { 3 return value; 4} 5 6function identityNumber(value: number): number { 7 return value; 8} 9 10// With generics - one function for all types 11function identity<T>(value: T): T { 12 return 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:

1function getFirst<T>(items: T[]): T | undefined { 2 return items[0]; 3} 4 5// Usage 6const firstNumber = getFirst<number>([1, 2, 3]); // number | undefined 7const firstString = getFirst<string>(['a', 'b', 'c']); // string | undefined 8 9// TypeScript can infer the type 10const 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.

Generic Interfaces

Interfaces can also be generic:

1interface Box<T> { 2 value: T; 3 getValue(): T; 4 setValue(value: T): void; 5} 6 7// Usage 8const numberBox: Box<number> = { 9 value: 42, 10 getValue() { 11 return this.value; 12 }, 13 setValue(value: number) { 14 this.value = value; 15 }, 16};

Generic Classes

Classes can use generics too:

1class Stack<T> { 2 private items: T[] = []; 3 4 push(item: T): void { 5 this.items.push(item); 6 } 7 8 pop(): T | undefined { 9 return this.items.pop(); 10 } 11} 12 13// Usage 14const numberStack = new Stack<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 property 2function getLength<T extends { length: number }>(item: T): number { 3 return item.length; 4} 5 6// Works with arrays, strings, etc. 7getLength([1, 2, 3]); // 3 8getLength('hello'); // 5 9// getLength(42); // Error: number doesn't have length

Using keyof with Constraints

A powerful pattern is constraining to object keys:

1function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 2 return obj[key]; 3} 4 5interface User { 6 id: string; 7 name: string; 8 email: string; 9} 10 11const user: User = { 12 id: '1', 13 name: 'John', 14 email: 'john@example.com', 15}; 16 17const name = getProperty(user, 'name'); // string 18// const invalid = getProperty(user, 'age'); // Error: 'age' doesn't exist

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 element 2function getLast<T>(items: T[]): T | undefined { 3 return items[items.length - 1]; 4} 5 6// Filter by property 7function filterBy<T, K extends keyof T>( 8 items: T[], 9 key: K, 10 value: T[K] 11): T[] { 12 return items.filter((item) => item[key] === value); 13} 14 15// Usage 16interface Person { 17 name: string; 18 age: number; 19} 20 21const people: Person[] = [ 22 { name: 'Alice', age: 30 }, 23 { name: 'Bob', age: 25 }, 24]; 25 26const adults = filterBy(people, 'age', 30); // Person[]

API Response Wrapper

1interface ApiResponse<T> { 2 data: T; 3 status: number; 4 message?: string; 5} 6 7async function fetchData<T>(url: string): Promise<ApiResponse<T>> { 8 const response = await fetch(url); 9 const data = await response.json(); 10 return { 11 data, 12 status: response.status, 13 }; 14} 15 16// Usage 17interface User { 18 id: string; 19 name: string; 20} 21 22const userResponse = await fetchData<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

  1. Leverage Type Inference
1// TypeScript can infer the type 2const result = identity('hello'); // string 3 4// Only specify when necessary 5const result2 = identity<string | null>(null); // string | null
  1. Use Constraints Judiciously
1// Good: Constraint enables functionality 2function getLength<T extends { length: number }>(item: T): number { 3 return item.length; 4} 5 6// Avoid: Unnecessary constraint limits flexibility 7function process<T extends string>(value: T): T { 8 return value; 9} 10// Better: Let it work with any type 11function process<T>(value: T): T { 12 return value; 13}
  1. Combine Generics with Utility Types
1function update<T>(obj: T, updates: Partial<T>): T { 2 return { ...obj, ...updates }; 3} 4 5function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { 6 const result = {} as Pick<T, K>; 7 keys.forEach((key) => { 8 result[key] = obj[key]; 9 }); 10 return result; 11}

Conclusion

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.

References

GET IN TOUCH

Let's work together

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.

WRITE AN EMAIL

or reach out directly at hello@mohammadshehadeh.com