TypeScript Generics
Master generics to build reusable, type-safe functions, classes, and components in TypeScript.
TL;DR
- 01Use type parameters to write reusable, type-safe code.
- 02Constrain generics to restrict what types they accept.
- 03Provide default types to simplify common generic usage.
Tips
- 01Name generics meaningfully, like <code>TItem</code> or <code>TResponse</code>, when single letters make the code harder to read.
Warnings
- 01Avoid overusing generics in simple functions, since they can make code harder to read and maintain.
Basics
- Add a type parameter in angle brackets to make a function generic.
function identity<T>(value: T): T { return value; } - The letter
Tis a placeholder that gets replaced with a real type. - TypeScript infers the type from the argument you pass to the function.
identity("hello"); // T inferred as string identity(42); // T inferred as number - You can also pass the type explicitly using angle brackets at the call.
identity<string>("hello"); - Use single uppercase letters or short descriptive names for type parameters.
Generic Functions
- Write a generic function once and reuse it for many different types.
function first<T>(items: T[]): T | undefined { return items[0]; } - Return values keep their original type, unlike using
anyfor parameters. - Pass arrays generically with
T[]orArray<T>for typed list operations. - Use multiple type parameters when handling pairs of types.
function pair<T, U>(a: T, b: U): [T, U] { return [a, b]; } - Chain generics through helper functions to keep type information intact.
Constraints
- Limit a type parameter using
extendsto require certain properties.function byId<T extends { id: number }>(items: T[], id: number) { return items.find(i => i.id === id); } - Constraints make sure the type has the properties your function needs.
- Use
keyof Tto constrain a parameter to keys of another type.function getValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } - Combine constraints with conditional types for advanced type narrowing.
- Constraints still allow types that include extra properties beyond the requirement.
Default Types
- Set a default type to simplify common use cases for callers.
interface ApiResponse<T = unknown> { data: T; status: number; } - Default types let callers skip the type argument when the default fits.
const res: ApiResponse = { data: null, status: 200 }; - Combine defaults with constraints to keep types both safe and easy.
type Config<T extends object = {}> = { values: T }; - Use defaults to keep public APIs clean while supporting custom overrides.
- Defaults are especially useful for generic React components and utility types.
Generic Classes
- Add type parameters to class names for typed instances and methods.
class Box<T> { constructor(public value: T) {} get() { return this.value; } } - Use the type parameter inside properties, methods, and constructor signatures.
- Pass the type when creating an instance, or let it be inferred.
const a = new Box<string>("hello"); const b = new Box(42); // inferred as Box<number> - Generic classes work well for collections, queues, stacks, and state stores.
- TypeScript will often infer the type from the constructor argument automatically.
FAQ
Use the extends keyword to restrict what types are accepted, e.g., function getLength<T extends { length: number }>(arg: T): number. This lets TypeScript guarantee properties exist on the type parameter without widening to any.
Use generics when the relationship between input and output types matters — function first<T>(arr: T[]): T preserves that the return type matches the array element type, which any would lose. Union types are better when a fixed set of known types is acceptable and reusability across unknown types isn't needed.
TypeScript infers type parameters from call-site arguments in most cases, so wrap(42) on function wrap<T>(val: T): T[] resolves T as number without annotation. Explicit type arguments like wrap<number>(42) are only needed when inference fails or you want to enforce a stricter type than what would be inferred.
Assign a default with =, like type Result<T = string> = { data: T; error: string | null }, so callers can omit the type argument when the default covers their case. This is particularly useful for generic utility types where one type dominates most usage but the generic remains available for edge cases.
Declare the type parameter on the class itself — class Repository<T> { findById(id: number): T {} } — so every method and property in the class operates on the same T. This differs from a generic method, where T is only in scope for that single method call.