TypeScript Generics Explained with Practical Examples
Learn TypeScript generics through practical examples for functions, components, API responses, constraints, utility types, and reusable code.
Generics preserve type information in reusable code
TypeScript generics let you write code that works with different types while preserving type information. Without generics, reusable functions often fall back to any, which removes safety, or they become duplicated for each type. Generics provide a middle path: flexible code with specific results. They are one of the most useful TypeScript features once the basic idea clicks.
A simple example is an identity function. If a function returns the same value it receives, the return type should match the input type. With function identity<T>(value: T): T, TypeScript captures the type of the argument and reuses it for the return value. If you pass a string, the result is a string. If you pass a number, the result is a number. The function stays reusable without losing precision.
Arrays and API responses are common examples
Generics become more valuable with data structures. A function that returns the first item in an array can be written as function first<T>(items: T[]): T | undefined. This works for arrays of users, strings, products, or anything else. The caller gets the correct item type without manual casting. That is the practical benefit: fewer assumptions and better editor support.
API responses are another common use case. Many applications receive responses shaped like { data, error, meta }. Instead of defining separate response types for every endpoint, you can write type ApiResponse<T> = { data: T; error?: string; meta?: PaginationMeta }. A user endpoint can return ApiResponse<User>, while a search endpoint can return ApiResponse<SearchResult[]>. The wrapper stays consistent, and the payload remains specific.
- Use generics when a function or type preserves a relationship between values.
- Add constraints when the generic needs required properties.
- Prefer readable type names over clever one-letter chains in complex code.
- Simplify generic types when they become harder to maintain than duplicate code.
Constraints make flexible code safer
Constraints help when a generic type needs certain properties. Suppose a function logs an object by ID. You can write function getId<T extends { id: string }>(item: T): string. The generic still accepts many object shapes, but it requires an id. This is safer than accepting any object and hoping the property exists. Constraints are useful for table rows, entity models, select options, and normalized data.
Generics also power reusable UI components. A table component can accept rows of type T and column definitions that reference keys of T. This lets the component render different data sets while catching invalid column names at compile time. The goal is not to make the type system clever for its own sake. The goal is to prevent mismatches between data and UI configuration.
Good generic design stays readable
Default generic parameters can make APIs easier to use. For example, a result type might default its error shape to string but allow a custom error object when needed. This keeps simple cases simple and advanced cases flexible. Good generic design often means choosing defaults that match common usage.
Generics can become hard to read when overused. Deeply nested conditional types and clever inference tricks may impress type enthusiasts but confuse product teams. If a generic type requires a long explanation every time someone touches it, consider simplifying. TypeScript should make code easier to change, not turn every feature into a puzzle.
The best way to learn generics is to look for repeated shapes in real code: lists, responses, forms, tables, events, and repositories. Start with simple type parameters, add constraints only when needed, and keep names meaningful. Generics are not magic. They are a tool for expressing relationships between inputs and outputs so reusable code remains safe.