Mastering TypeScript Generics
Deep dive into TypeScript generics — from basic concepts to advanced patterns like conditional types, mapped types, and template literal types.
TypeScript generics are one of the most powerful features of the type system, yet they often intimidate developers new to the language. In this article, we'll build up from the fundamentals to advanced patterns that will transform how you write type-safe code.
The Basics
At its core, a generic is a type variable — a placeholder for a type that will be specified later. Think of it like a function parameter, but for types:
function identity<T>(value: T): T {
return value;
}
const result = identity("hello"); // type is string
const num = identity(42); // type is numberThe T is a type parameter. When you call identity("hello"), TypeScript infers that T should be string, and the return type is also string. This gives you type safety without having to write overloads for every possible type.
Constraints
Sometimes you need to restrict what types can be used. You can add constraints with the extends keyword:
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(value: T): number {
return value.length;
}
getLength("hello"); // works — strings have .length
getLength([1, 2, 3]); // works — arrays have .length
getLength(123); // Error — numbers don't have .lengthThis pattern is incredibly useful for writing utility functions that work with any type that satisfies a certain shape.
Generic Interfaces and Classes
Generics aren't limited to functions. You can use them in interfaces and classes too:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type UserResponse = ApiResponse<{ name: string; email: string }>;
type PostResponse = ApiResponse<{ title: string; content: string }>;
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return [...this.items];
}
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
}Conditional Types
Conditional types let you create types that depend on other types, similar to ternary expressions:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical example: extract the return type of a function
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type FnReturn = ReturnTypeOf<() => string>; // stringThe infer keyword is particularly powerful — it lets you "extract" a type from within a complex type structure.
Mapped Types
Mapped types allow you to create new types by transforming every property in an existing type:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }
type PartialUser = Optional<User>;
// { name?: string; age?: number; email?: string }Template Literal Types
One of the most exciting additions to TypeScript, template literal types let you manipulate string types at the type level:
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"This opens up possibilities for creating strongly-typed APIs for CSS-in-JS libraries, event systems, and more.
Practical Patterns
Here's a real-world pattern — a type-safe API client:
interface Endpoints {
"/users": { response: User[]; method: "GET" };
"/users/:id": { response: User; method: "GET" };
"/posts": { response: Post[]; method: "GET" };
"/posts": { response: Post; method: "POST"; body: CreatePostDTO };
}
async function apiCall<Path extends keyof Endpoints>(
path: Path,
options: Endpoints[Path]["method"] extends "POST"
? { body: Endpoints[Path] extends { body: infer B } ? B : never }
: {}
): Promise<Endpoints[Path]["response"]> {
// implementation
}TypeScript generics are a deep topic, but mastering them pays dividends in code quality, developer experience, and maintainability. Start with the basics, practice with real problems, and gradually explore the more advanced patterns.