
Understanding TypeScript Generics: From Basics to Advanced Patterns
A comprehensive guide to TypeScript generics — covering everything from simple type parameters to advanced patterns like conditional types, mapped types, and template literals.
Why Generics Matter
TypeScript generics are one of the most powerful features of the language. They allow you to write reusable, type-safe code that works with multiple types while maintaining the benefits of static typing.
Think of generics as type variables — they let you capture the type of an argument so you can use it throughout your function, class, or interface.
Getting Started with Generics
The Problem Generics Solve
Without generics, you'd have to either use any (losing type safety) or create duplicate functions for each type:
// ❌ Using any — no type safety
function identity(arg: any): any {
return arg;
}
// ❌ Duplicated for each type
function identityNumber(arg: number): number {
return arg;
}
function identityString(arg: string): string {
return arg;
}
The Generic Solution
// ✅ Generic — preserves type information
function identity<T>(arg: T): T {
return arg;
}
const num = identity(42); // type: number
const str = identity("hello"); // type: string
const bool = identity(true); // type: boolean
Generic Constraints
Sometimes you need to restrict what types can be used with a generic. That's where constraints come in:
interface HasLength {
length: number;
}
// T must have a length property
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
logLength({ length: 10 }); // ✅ object with length
// logLength(42); // ❌ number doesn't have length
Using keyof with Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Vincent", age: 25, role: "developer" };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
// getProperty(user, "email"); // ❌ Property 'email' doesn't exist
Generic Interfaces and Types
Generic Interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: string;
name: string;
email: string;
}
type UserResponse = ApiResponse<User>;
// {
// data: User;
// status: number;
// message: string;
// }
Generic Type Aliases
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
const successResult: Result<number> = {
success: true,
value: 42,
};
const errorResult: Result<string> = {
success: false,
error: new Error("Something went wrong"),
};
Advanced Generic Patterns
Conditional Types
Conditional types allow you to create types that depend on a condition:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Inferring Types
The infer keyword lets you extract types from other types:
// Extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (a: number, b: string) => boolean;
type FnReturn = ReturnType<Fn>; // boolean
// Extract promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Promised = Promise<string>;
type Unwrapped = Unwrap<Promised>; // string
Mapped Types
Mapped types let you transform existing types:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// {
// readonly name: string;
// readonly age: number;
// }
Template Literal Types
type EventName = `on${Capitalize<string>}`;
type EventHandler<T extends EventName> = T extends `on${infer E}`
? (event: E) => void
: never;
type ClickHandler = EventHandler<"onClick">; // (event: "Click") => void
Practical Examples
Type-Safe API Client
interface ApiConfig {
baseUrl: string;
headers?: Record<string, string>;
}
class ApiClient {
constructor(private config: ApiConfig) {}
async get<T>(path: string): Promise<T> {
const response = await fetch(`${this.config.baseUrl}${path}`, {
headers: this.config.headers,
});
return response.json();
}
async post<T, U>(path: string, body: U): Promise<T> {
const response = await fetch(`${this.config.baseUrl}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...this.config.headers,
},
body: JSON.stringify(body),
});
return response.json();
}
}
const api = new ApiClient({ baseUrl: "https://api.example.com" });
// Types are inferred automatically
const user = await api.get<User>("/users/1");
const created = await api.post<User, CreateUserDto>("/users", {
name: "Vincent",
email: "vincent@example.com",
});
Generic React Component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>;
Best Practices
- Use descriptive names —
Tis fine for simple cases, but useTItem,TResponse, etc., for clarity - Constrain when needed — don't over-constrain, but don't leave types too loose
- Prefer inference — let TypeScript infer generic types when possible
- Keep it simple — don't over-engineer with complex generics if a simpler solution works
Conclusion
Generics are what make TypeScript truly powerful. They enable you to write flexible, reusable code without sacrificing type safety. Start simple, and as you get comfortable, explore the more advanced patterns. Your codebase will thank you.
What's your favorite TypeScript feature? I regularly share TypeScript tips on Twitter. Follow me for more.
Related Articles

Building Performant React Applications: A Practical Guide
Learn the essential techniques for building fast, responsive React applications — from code splitting and lazy loading to memoization and state management optimization.
Why Every Small and Medium Business Needs a Professional Website in 2026
Discover how a modern website helps small and medium businesses attract more customers, build trust, and increase sales

Getting Started with Next.js 16: What's New and Why You Should Care
Next.js 16 brings groundbreaking features including the Turbopack stable release, React 19 support, and enhanced server actions. Here's everything you need to know.