TypeScript Generics: A Practical Guide
Introduction
Generics are where TypeScript separates developers who use it from developers who understand it. Most people learn the basics — Array<T>, Promise<T> — and then stop. They write any when the types get complex, or reach for overloads when a single generic would be cleaner.
This guide is about bridging that gap. Not theory for theory's sake, but patterns you'll use in real code — API response types, utility functions, hook abstractions, and type-safe builder patterns. By the end, generics shouldn't feel like a puzzle to solve. They should feel like a natural tool.
Core Concepts
What Generics Actually Are
A generic is a type variable — a placeholder that gets filled in when the function, class, or interface is used. It lets you write logic once and have it work correctly across different types without sacrificing type safety.
Without generics:
function first(arr: number[]): number {
return arr[0];
}
// Can't use this with strings, objects, etc.With generics:
function first<T>(arr: T[]): T {
return arr[0];
}
first([1, 2, 3]); // T = number → returns number
first(['a', 'b', 'c']); // T = string → returns string
first([{ id: 1 }]); // T = { id: number } → returns { id: number }TypeScript infers T from the argument. You rarely need to specify it explicitly.
Constraints: Narrowing What T Can Be
Sometimes you need to restrict what types T can be. Constraints use extends:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Shavrka', age: 25 };
getProperty(user, 'name'); // OK → string
getProperty(user, 'age'); // OK → number
getProperty(user, 'email'); // ERROR — 'email' doesn't exist on userK extends keyof T ensures key must be an actual property of obj. TypeScript even infers the return type correctly based on which key you pass.
Default Type Parameters
Like function parameters, generics can have defaults:
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
// T defaults to unknown if not specified
const response: ApiResponse = { data: {}, status: 200, message: 'OK' };
// Specified explicitly
const userResponse: ApiResponse<User> = { data: user, status: 200, message: 'OK' };