Generics are often considered one of the scariest features for new TypeScript developers. You see angle brackets <T> everywhere and it looks like complex algebra. But don't worry—Generics are actually quite simple once you understand the problem they solve.
The Problem: Reusability vs. Type Safety
Imagine you want to write a function that returns whatever you pass into it. Without generics, you have two bad options:
Option 1: Specific Types
function returnNumber(arg: number): number {
return arg;
}
function returnString(arg: string): string {
return arg;
}This is not reusable. You have to write a new function for every single type.
Option 2: The 'any' Type
function returnAnything(arg: any): any {
return arg;
}This is not type-safe. TypeScript loses all information about what the data is. If you pass a number, TypeScript forgets it's a number and treats it as any.
The Solution: Generics
Generics allow you to create a "Type Variable". Just like a function argument allows your function to accept different values, a Generic allows your function to accept different types.
function identity<T>(arg: T): T {
return arg;
}Here, T is a placeholder. When you call the function, TypeScript captures the type you used and replaces T with it.
const output = identity<string>("Hello");
// output is strictly typed as 'string'Real-World Example: API Responses
The most common place you will use generics is when fetching data. Imagine your API always wraps data in a standard format:
interface ApiResponse<T> {
status: number;
message: string;
data: T;
}Now you can reuse this interface for any data model in your app:
interface User {
id: number;
name: string;
}
interface Product {
id: number;
price: number;
}
// Reusing the generic interface
const userResponse: ApiResponse<User> = ...;
const productResponse: ApiResponse<Product> = ...;Generic Constraints
Sometimes you want to be generic, but not too generic. You can use extends to limit what types are allowed.
For example, if you want to write a function that logs the .length of an item, you need to ensure the item actually has a length property.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
logLength("Hello"); // Works (string has .length)
logLength([1, 2, 3]); // Works (array has .length)
logLength(100); // Error! (number does not have .length)Conclusion
Generics are the key to writing libraries and reusable utilities in TypeScript. They bridge the gap between flexibility and strictness, allowing you to write code that is both dynamic and safe.