TypeScript Generics Explained: A Practical Guide with Real Examples - By Sourav Mishra (@souravvmishra)
Master TypeScript generics with practical examples. Learn type parameters, constraints, utility types, and patterns used in production codebases.
Generics are one of TypeScript's most powerful features. They allow you to write flexible, reusable code while maintaining full type safety. Yet, many developers find them intimidating at first.
In this post, I'll break down generics with practical examples you'll actually use in your projects.
What Problem Do Generics Solve?
Imagine you're building a function that returns the first element of an array:
function getFirst(arr: number[]): number {
return arr[0];
}
This works for numbers, but what about strings? Or objects? You'd need to write a separate function for each type, or resort to any:
function getFirst(arr: any[]): any {
return arr[0];
}
Using any defeats the purpose of TypeScript. We lose all type checking. Generics solve this.
The Generic Solution
With generics, we tell TypeScript: "I don't know the type yet, but whatever type goes in should also come out."
function getFirst<T>(arr: T[]): T {
return arr[0];
}
// TypeScript infers the return type
const num = getFirst([1, 2, 3]); // type: number
const str = getFirst(["a", "b", "c"]); // type: string
The <T> is a type parameter. It's a placeholder that gets replaced with the actual type when the function is called.
Real-World Example: API Response Wrapper
Here's a pattern I use constantly. When fetching data from APIs, you often have a consistent response shape with varying data:
interface ApiResponse<T> {
data: T;
success: boolean;
message: string;
timestamp: number;
}
interface User {
id: string;
name: string;
email: string;
}
interface Product {
id: string;
title: string;
price: number;
}
// Now we can type our API calls precisely
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function fetchProducts(): Promise<ApiResponse<Product[]>> {
const res = await fetch("/api/products");
return res.json();
}
The ApiResponse<T> interface is reusable across your entire codebase, while each endpoint gets precise typing.
Constraining Generics
Sometimes you need to restrict what types can be used. Use the extends keyword:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Works - User has an id property
const user = findById(users, "123");
// Error - number[] doesn't have id property
const num = findById([1, 2, 3], "123"); // ❌ Type error
This ensures T always has an id property, so item.id is safe to access.
Multiple Type Parameters
You can use multiple generics when needed:
function createPair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const pair = createPair("age", 25); // type: [string, number]
A practical example is a generic Map-like structure:
function groupBy<T, K extends keyof T>(
items: T[],
key: K
): Map<T[K], T[]> {
const map = new Map<T[K], T[]>();
for (const item of items) {
const groupKey = item[key];
const group = map.get(groupKey) || [];
group.push(item);
map.set(groupKey, group);
}
return map;
}
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Carol", role: "admin" }
];
const byRole = groupBy(users, "role");
// Map<string, User[]>
Default Type Parameters
Like function parameters, generics can have defaults:
interface PaginatedResponse<T, M = null> {
items: T[];
total: number;
page: number;
meta: M;
}
// M defaults to null
type UserList = PaginatedResponse<User>;
// Or specify custom metadata
type ProductList = PaginatedResponse<Product, { category: string }>;
Common Generic Utility Types
TypeScript includes built-in generic utility types. Here are the ones I use most:
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick specific properties
type UserPreview = Pick<User, "id" | "name">;
// Exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// Extract function return type
type FetchUserReturn = ReturnType<typeof fetchUser>;
Key Takeaways
- Generics preserve type information - Unlike
any, types flow through your code - Start simple - Begin with
<T>and add constraints as needed - Use constraints -
extendsensures your generic has the properties you need - Leverage built-in utilities -
Partial,Pick,Omitsave you from reinventing the wheel
Generics might feel abstract at first, but once you start using them, you'll wonder how you ever wrote TypeScript without them.
The best way to learn? Start refactoring your existing code to use generics wherever you see repeated patterns with different types.
Using TypeScript with Next.js? Check out my guide on Server Actions for type-safe mutations.
Frequently Asked Questions
Q: What are TypeScript generics?
TypeScript generics are type parameters that let you write reusable, type-safe code. They act as placeholders for types that are specified when the function or class is used, preserving type information throughout your code.
Q: When should I use generics in TypeScript?
Use generics when you have a function, class, or interface that works with multiple types but needs to maintain type safety. Common use cases include utility functions, API response wrappers, and data structures like arrays or maps.
Q: What's the difference between generics and any?
any disables type checking entirely - you lose all TypeScript benefits. Generics preserve type information, so TypeScript can still catch errors and provide autocomplete. Use generics for flexibility with safety; avoid any in production code.
Q: How do I constrain a generic type?
Use the extends keyword: function example<T extends HasId>(item: T). This ensures T has at least the properties defined in HasId, allowing safe access to those properties.