TypeScript Generics: A Practical Guide
Learn TypeScript generics from the ground up with practical examples covering generic functions, constraints, default types, and generic React-style utilities.
Introduction
Generics are one of the features that separate "I know some TypeScript" from "I can write reusable, type-safe TypeScript." They let you build functions, interfaces, and classes that work across many types without giving up type safety or resorting to any. If you have ever written the same function twice just to change one type, generics are the fix.
The Problem Generics Solve
Imagine a function that wraps a value in an array. Without generics, you either lose type information or duplicate the function per type:
function wrapInArrayUnsafe(value: any): any[] {
return [value];
}
const result = wrapInArrayUnsafe(42);
// result is typed as any[], so TypeScript can't help you here
With a generic type parameter, TypeScript preserves the exact type through the function:
function wrapInArray<T>(value: T): T[] {
return [value];
}
const numbers = wrapInArray(42); // number[]
const strings = wrapInArray("hello"); // string[]
T is a placeholder — TypeScript infers it from the argument you pass, and the return type automatically reflects that same type.
Generic Functions with Multiple Type Parameters
Generics are not limited to one type parameter:
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const result = pair("id", 42); // [string, number]
Generic Interfaces and Type Aliases
Generics apply to object shapes too, which is extremely common for API responses:
interface ApiResponse<T> {
data: T;
success: boolean;
error?: string;
}
interface User {
id: number;
name: string;
}
const response: ApiResponse<User> = {
data: { id: 1, name: "Ada" },
success: true,
};
The same ApiResponse shape can now describe a response containing a User, a Product, or an array of either, without rewriting the interface each time.
Constraining Generics with extends
Sometimes a generic type needs to guarantee it has certain properties. The extends keyword constrains what can be passed in:
function getId<T extends { id: number }>(item: T): number {
return item.id;
}
getId({ id: 1, name: "Ada" }); // OK
getId({ name: "no id here" }); // Error: missing property 'id'
This keeps the flexibility of generics while still enforcing the minimum shape your function actually needs.
Default Type Parameters
You can give a generic parameter a default, used when TypeScript cannot infer one and the caller does not specify it:
interface Cache<T = string> {
get(key: string): T | undefined;
set(key: string, value: T): void;
}
const stringCache: Cache = { /* T defaults to string */
get: () => undefined,
set: () => {},
};
A Generic Class Example: Stack
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20
Generics with Built-in Utility Types
TypeScript's built-in utility types are themselves generic, and understanding generics helps you use them well:
interface Todo {
id: number;
title: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "id" | "title">;
type PartialTodo = Partial<Todo>;
type ReadonlyTodo = Readonly<Todo>;
Pick<T, K>, Partial<T>, and Readonly<T> are all generic types that transform another type — the same mental model as the generic functions above.
Best Practices
- Name generic parameters meaningfully in complex code (
TItem,TResponse) rather than always using a bareTwhen there are several. - Constrain generics with
extendswhenever your function relies on specific properties, instead of casting inside the function body. - Prefer letting TypeScript infer generic arguments from the call site rather than specifying them manually every time.
- Use generic interfaces for anything shaped like a container — API responses, caches, collections — instead of duplicating interfaces per type.
- Reach for the built-in utility types (
Partial,Pick,Omit,Record) before writing your own generic helper; they cover most common cases.
Common Mistakes to Avoid
- Adding a generic parameter that is never actually used to constrain or connect two parts of a function — if
Tonly appears once, you may not need generics at all. - Over-constraining generics so tightly that the function loses the flexibility that made generics worth using in the first place.
- Using
anyinstead of a generic parameter "to make the error go away," which removes the type safety generics were meant to add. - Forgetting that class-level generics (
class Stack<T>) apply to every method in the class, not just one.
Combining Generics with keyof
A particularly powerful pattern combines a generic type parameter with keyof, letting you write a function that safely accesses any property of an object without knowing its exact shape ahead of time:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada", email: "ada@example.com" };
const name = getProperty(user, "name"); // inferred as string
const id = getProperty(user, "id"); // inferred as number
const bad = getProperty(user, "unknown"); // Error: "unknown" is not a key of user
Here, K extends keyof T constrains the second parameter to only the actual property names of T, and the return type T[K] looks up exactly what type that specific property holds. This gives you a generic, reusable accessor function that remains fully type-safe — TypeScript will reject any key that does not really exist on the object, and it will correctly infer the return type based on which key you passed in. This pattern shows up constantly in utility libraries, form libraries, and anywhere you need to work generically with "some property of some object" while keeping full type safety intact.
Generic Defaults for Simpler Call Sites
Generic parameters can have default types, which lets callers omit the type argument entirely in the common case while still allowing an explicit override when needed:
interface ApiResponse<TData = unknown> {
data: TData;
status: number;
message: string;
}
function createEmptyResponse(): ApiResponse {
return { data: undefined, status: 204, message: "No Content" };
}
const userResponse: ApiResponse<{ id: number; name: string }> = {
data: { id: 1, name: "Ada" },
status: 200,
message: "OK",
};
Without the default, every single usage of ApiResponse — even ones that genuinely do not care about the shape of data — would be forced to supply a type argument or fall back to an implicit any. The default TData = unknown means simple cases stay simple, while call sites that do care about the payload shape can still specify it explicitly. This same pattern shows up in many popular libraries' type definitions, where a generic component or function has a sensible default that covers 80% of use cases without requiring every consumer to think about type parameters at all.
A good habit when writing your own generic functions is to let TypeScript infer type arguments from usage whenever possible, rather than requiring callers to specify them explicitly. If a function's parameter types already reference the generic parameter — as in function identity<T>(value: T): T — TypeScript infers T automatically from whatever argument is actually passed, and explicit type arguments become an escape hatch for the rare case where inference alone cannot determine the right type, rather than something every caller needs to write out by hand.
Conclusion
Generics let you write less code while keeping more type safety — the opposite of what usually happens when you try to make code more flexible. Once you get comfortable with a plain <T>, move on to constraints with extends, then generic interfaces, and finally generic classes. Each layer builds naturally on the last.
Article FAQ
References
Comments
Comments are coming soon. Meanwhile, share your feedback via our contact page.
Related Articles
View allGetting Started with TypeScript Types
A beginner-friendly introduction to TypeScript's type system, covering primitives, interfaces, unions, type inference, and how to add types to existing JavaS...
System Design Interview Preparation Guide
A structured approach to preparing for system design interviews, covering the framework interviewers expect, common topics to study, and how to communicate y...
Top JavaScript Interview Questions and Answers
A curated list of common JavaScript interview questions with clear explanations, covering closures, hoisting, the event loop, prototypes, and equality compar...
Resume Tips for Developers
Practical resume tips for software developers, covering how to describe technical work with impact, formatting advice, and what to leave off a strong develop...