TypeScript generics are one of the most powerful features of the type system. They allow you to write flexible, reusable, and type-safe code by parameterizing types. Instead of duplicating functions or classes for every data type, generics let you write once and use with any type while maintaining full type safety. This guide covers everything from basics to advanced patterns.
Your First Generic Function
The classic starting point is the identity function — a function that returns exactly what it receives. Without generics, you would either lose type information by using any, or you would have to write separate functions for each type.
With generics, you define a type parameter (conventionally named T) that acts as a placeholder. When you call the function, TypeScript infers the actual type automatically.
// Without generics — loses type info
function identityAny(value: any): any {
return value;
}
const result1 = identityAny("hello"); // type: any (not string!)
// With generics — preserves type info
function identity<T>(value: T): T {
return value;
}
const result2 = identity("hello"); // type: string ✓
const result3 = identity(42); // type: number ✓
const result4 = identity(true); // type: boolean ✓
// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: stringGeneric Functions
Generic functions can accept multiple type parameters and even have default types. This makes them incredibly versatile for building utility functions.
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // type: [string, number]
// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]
// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, "x"); // T inferred as string
const numbers = createArray(3, 0); // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to stringDefault type parameters work just like default function parameters — if the caller does not specify a type, the default is used.
Generic Interfaces and Types
Generics are not limited to functions. You can parameterize interfaces and type aliases to create flexible data structures that work with any type.
// Generic interface
interface Box<T> {
value: T;
label: string;
}
const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };
// Generic interface with multiple params
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// Generic type alias
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };
// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;
// Generic type for nullable values
type Nullable<T> = T | null | undefined;Generic interfaces are especially useful for defining shapes of API responses, collections, and data containers.
Generic Classes
Classes can also be generic. This is useful for building data structures like stacks, queues, linked lists, or any container that should work with multiple types.
// Generic Stack class
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(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined
// Generic class with multiple type params
class HashMap<K, V> {
private map = new Map<K, V>();
set(key: K, value: V): void {
this.map.set(key, value);
}
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
}The type parameter is available throughout the class — in properties, method parameters, and return types.
Generic Constraints with extends
Unconstrained generics accept any type, but sometimes you need to narrow down what types are allowed. The extends keyword lets you add constraints.
Constraining to Object Keys with keyof
A very common pattern is constraining a type parameter to be a valid key of another type. This ensures type-safe property access.
// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // type: string ✓
getProperty(user, "age"); // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"
// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }Constraining to Specific Shapes
You can require that a type parameter has certain properties by extending an interface or object type.
// Constraint to objects with a length property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(`Length: ${value.length}`);
return value;
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42); // Error — number has no length property
// Constraint to objects with an id
interface HasId {
id: number | string;
}
function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const found = findById(users, 1);
// type: { id: number; name: string } | undefinedConditional Types
Conditional types let you choose between two types based on a condition, using the syntax T extends U ? X : Y. They are the foundation of many advanced type-level computations.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type D = Flatten<string[]>; // string
type E = Flatten<number[][]>; // number[]
type F = Flatten<string>; // string (not an array, returned as-is)
// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type G = MyReturnType<() => string>; // string
type H = MyReturnType<(x: number) => boolean>; // boolean
// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>; // numberConditional types become especially powerful when combined with infer, which lets you extract types from within other types.
Mapped Types
Mapped types let you transform the properties of an existing type to create a new type. They iterate over keys using [K in keyof T] syntax.
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required (like Required<T>)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
interface User {
name: string;
age: number;
active: boolean;
}
type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }
// Mapped type with conditional
type OptionalNullable<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};You can add or remove modifiers like readonly and ? (optional) in mapped types.
Built-in Utility Types
TypeScript ships with many utility types built using generics. Here are the most commonly used ones:
| Utility Type | Description | Example |
|---|---|---|
Partial<T> | Makes all properties optional | Partial<User> |
Required<T> | Makes all properties required | Required<Partial<User>> |
Pick<T, K> | Picks specified properties | Pick<User, "name" | "email"> |
Omit<T, K> | Omits specified properties | Omit<User, "password"> |
Record<K, V> | Creates a key-value type mapping | Record<string, number> |
Readonly<T> | Makes all properties readonly | Readonly<Config> |
ReturnType<T> | Extracts function return type | ReturnType<typeof fn> |
Parameters<T> | Extracts function parameter types tuple | Parameters<typeof fn> |
// Utility types in action
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// For update forms — all fields optional
type UpdateUser = Partial<User>;
// Public user — omit password
type PublicUser = Omit<User, "password">;
// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;
// Lookup table
type UserLookup = Record<number, User>;
// Immutable user from API
type FrozenUser = Readonly<User>;
// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [number]Real-World Patterns
Generic API Response Wrapper
One of the most common uses of generics is wrapping API responses in a consistent structure. This ensures every endpoint returns data in the same format while preserving the specific data type.
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
}
// Usage with specific types
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓
const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓Generic React Component Props
Generics let you build React components that work with different data types while maintaining type safety on props.
// Generic list component props
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
onItemClick?: (item: T) => void;
emptyMessage?: string;
}
function GenericList<T>({
items,
renderItem,
keyExtractor,
onItemClick,
emptyMessage = "No items",
}: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, index) => (
<li
key={keyExtractor(item)}
onClick={() => onItemClick?.(item)}
>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage — TypeScript infers T from items
<GenericList
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
onItemClick={(user) => console.log(user.email)}
/>Generic Repository Pattern
The repository pattern abstracts data access behind a generic interface. This allows you to swap data sources without changing business logic.
// Generic repository interface
interface Entity {
id: number | string;
}
interface Repository<T extends Entity> {
findAll(): Promise<T[]>;
findById(id: T["id"]): Promise<T | null>;
create(data: Omit<T, "id">): Promise<T>;
update(id: T["id"], data: Partial<T>): Promise<T>;
delete(id: T["id"]): Promise<boolean>;
}
// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
constructor(private baseUrl: string) {}
async findAll(): Promise<T[]> {
const res = await fetch(this.baseUrl);
return res.json();
}
async findById(id: T["id"]): Promise<T | null> {
const res = await fetch(`${this.baseUrl}/${id}`);
if (!res.ok) return null;
return res.json();
}
async create(data: Omit<T, "id">): Promise<T> {
const res = await fetch(this.baseUrl, {
method: "POST",
body: JSON.stringify(data),
});
return res.json();
}
async update(id: T["id"], data: Partial<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return res.json();
}
async delete(id: T["id"]): Promise<boolean> {
const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
return res.ok;
}
}
// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]Generic Event Emitter
A type-safe event emitter ensures that event names and their payloads are correctly typed. No more string-based events with unknown data.
// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;
class TypedEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: EventCallback<Events[K]>[];
} = {};
on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
const cbs = this.listeners[event];
if (cbs) {
this.listeners[event] = cbs.filter(cb => cb !== callback);
}
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const cbs = this.listeners[event];
if (cbs) {
cbs.forEach(cb => cb(payload));
}
}
}
// Define your event types
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"item:added": { itemId: string; quantity: number };
"error": { message: string; code: number };
}
const emitter = new TypedEmitter<AppEvents>();
// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
console.log(payload.userId); // type: string ✓
console.log(payload.timestamp); // type: number ✓
});
// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});Common Mistakes
Here are the most frequent mistakes developers make with generics, along with corrections:
| Mistake | Problem | Correction |
|---|---|---|
| Using any instead of generics | Loses all type information | Use a type parameter T instead of any |
| Over-constraining type params | Makes the generic unusable for valid types | Only constrain to what you actually need |
| Unnecessary type parameters | Adding T that is used only once adds complexity | If T appears only in return type, just use the concrete type |
| Not using constraints when accessing properties | TypeScript error: Property does not exist on type T | Add extends constraint: T extends { prop: type } |
| Confusing generic defaults with constraints | Default type does not restrict — callers can still pass other types | Use extends for restriction, = for default |
| Not inferring when possible | Explicitly passing types when TS can infer them | Let TypeScript infer types from arguments when possible |
| Using T[] when you mean readonly T[] | Allows mutation of arrays that should be read-only | Use readonly T[] for immutable data from APIs |
// ❌ Mistake: Using any
function badParse(json: string): any {
return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!
// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
try {
const parsed = JSON.parse(json);
return validator(parsed) ? parsed : null;
} catch {
return null;
}
}
// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
return arr.length; // T is never meaningfully used
}
// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
return arr.length;
}
// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
return obj.name; // Error: Property 'name' does not exist on type 'T'
}
// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
return obj.name; // OK ✓
}Frequently Asked Questions
What is the difference between generics and the any type?
Generics preserve type information throughout the function or class, while any discards it. With generics, if you pass a string, TypeScript knows the return type is also string. With any, all type information is lost.
When should I use generics vs function overloads?
Use generics when the logic is the same for all types and you just need to preserve type relationships. Use overloads when the implementation differs based on input types, or when you need to map specific input types to specific output types.
Can I use generics with arrow functions in TSX files?
Yes, but you need to add a trailing comma to avoid JSX ambiguity: const fn = <T,>(arg: T): T => arg. The comma tells the parser this is a generic, not a JSX tag.
How many type parameters should a generic have?
As few as possible. One or two is common. Three is sometimes necessary. If you need more than three, consider refactoring — your abstraction may be too complex. Each type parameter should serve a clear purpose.
Do generics affect runtime performance?
No. Generics are purely a compile-time feature. They are completely erased during compilation and produce no runtime overhead. The generated JavaScript has no trace of generic type parameters.
What is the naming convention for type parameters?
Single uppercase letters are conventional: T for type, K for key, V for value, E for element, R for return type. For complex generics with multiple parameters, descriptive names like TData, TError are acceptable and improve readability.