TypeScript Generics: Writing Reusable Code
The Problem with `any`
TypeScript is amazing because it catches errors before your code even runs. But developers frequently hit a wall when writing functions that need to accept multiple data types. If you write a function that reverses an array, what type should the array be? If you define it as number[], it won't work for strings. If you define it as any[], you have defeated the entire purpose of TypeScript, completely losing autocomplete and type safety on the result.
Enter Generics: Type Variables
Generics allow you to write functions, classes, and interfaces that take Types as arguments, just like regular functions take values as arguments. You define a generic type parameter (usually denoted by a capital letter like <T>) and use it throughout your function.
// The T captures whatever type the user passes in
function reverseArray<T>(items: T[]): T[] {
return items.reverse();
}
// TypeScript knows 'numbers' is specifically an array of numbers
const numbers = reverseArray<number>([1, 2, 3]);
// TypeScript knows 'words' is specifically an array of strings
const words = reverseArray<string>(["hello", "world"]);
You don't even always need to write the <string> explicitly. TypeScript is incredibly smart with "Type Inference" and can usually guess what T is based on the data you pass in.
Real World Application: API Responses
Generics are absolutely mandatory when dealing with API responses. In a large enterprise application, you will have dozens of different endpoints returning different data objects (Users, Products, Invoices). However, the overall structure of an API response is usually the same: it contains a status code, an error message, and a data payload.
Instead of writing 50 different interfaces, you write one Generic Interface:
interface ApiResponse<T> {
status: number;
message: string;
data: T; // The shape of 'data' changes based on what we pass in
}
interface User { id: number; name: string; }
interface Product { id: number; price: number; }
// Using the Generic
async function fetchUser(): Promise<ApiResponse<User>> {
const res = await fetch('/api/user');
return res.json();
}
fetchUser().then(response => {
// TypeScript provides perfect autocomplete here!
// It knows response.data has a .name property.
console.log(response.data.name);
});
Generic Constraints
Sometimes you want a Generic to be flexible, but not too flexible. You can use the extends keyword to force the generic type to have certain properties. For example, if you write a function that logs the length of an object, you can ensure that T must at least have a .length property:
interface HasLength { length: number; }
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
logLength([1, 2, 3]); // Works, arrays have .length
logLength("Hello"); // Works, strings have .length
// logLength(42); // ERROR: Numbers do not have a .length property!