TypeScript Generics: From Basics to Advanced Patterns
Master TypeScript generics with practical examples — covering constraints, conditional types, mapped types, infer, and real-world utility patterns used in production codebases.
Why Generics Exist
The whole point of a type system is to catch errors before runtime. But a type system that forces you to write separate, identical functions for every possible type defeats the purpose — you end up with numberIdentity, stringIdentity, booleanIdentity, and twice the surface area for bugs.
Generics solve this by letting you write code that works with any type while still being fully type-safe. They are the most powerful, and most misunderstood, feature in TypeScript.
The Basics
A generic is a type variable — a placeholder for a type that will be resolved later.
// Without generics — loses type information
function identity(value: any): any {
return value;
}
// With generics — type is preserved
function identity<T>(value: T): T {
return value;
}
const n = identity(42); // n: number
const s = identity("hello"); // s: stringTypeScript infers T from the argument. You can also be explicit: identity<number>(42).
Generic Interfaces and Classes
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: string, partial: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> { /* ... */ }
async findAll(): Promise<User[]> { /* ... */ }
async create(entity: Omit<User, 'id'>): Promise<User> { /* ... */ }
async update(id: string, partial: Partial<User>): Promise<User> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}This pattern gives you a type-safe contract for all data access code. Swap the backing store (Prisma, Drizzle, raw SQL) without changing any calling code.
Constraints with extends
Unconstrained generics can be too permissive. Constraints restrict what types are accepted:
// T must have a 'length' property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "world"); // ✅ string has length
longest([1, 2, 3], [4, 5]); // ✅ array has length
longest({ length: 5 }, { length: 2 }); // ✅ object with length
// longest(42, 99); // ❌ number has no 'length'Constraining Keys with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: "1", name: "Yousuf", age: 22 };
getProperty(user, "name"); // string ✅
getProperty(user, "age"); // number ✅
// getProperty(user, "bio"); // ❌ 'bio' is not a key of typeof userConditional Types
Conditional types let the output type depend on an input type:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // falsePractical Example: Unwrapping Promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<boolean>; // booleanThis is actually how TypeScript's built-in Awaited<T> utility type works.
The infer Keyword
infer lets you extract and name a type from within a conditional type:
// 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 R = ReturnType<Fn>; // boolean
// Extract first element type of a tuple
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type H = Head<[string, number, boolean]>; // string
// Extract parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type P = Parameters<(x: number, y: string) => void>; // [number, string]Mapped Types
Mapped types transform every property in a type programmatically:
// Make all properties optional (built-in Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Nullify all properties
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Pick only specific properties
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};Remapping Keys with as
TypeScript 4.1+ allows remapping keys in mapped types:
// Convert all property names to uppercase
type UppercaseKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};
type Config = { apiUrl: string; timeout: number };
type UpperConfig = UppercaseKeys<Config>;
// { APIURL: string; TIMEOUT: number }
// Generate getter names
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }Template Literal Types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type DeepPath<T, Prefix extends string = ""> = {
[K in keyof T]: T[K] extends object
? DeepPath<T[K], `${Prefix}${string & K}.`>
: `${Prefix}${string & K}`;
}[keyof T];
type Config = {
db: { host: string; port: number };
api: { url: string };
};
type ConfigPath = DeepPath<Config>;
// "db.host" | "db.port" | "api.url"Real-World Patterns
Type-Safe Event Emitter
type EventMap = {
login: { userId: string; timestamp: Date };
logout: { userId: string };
error: { code: number; message: string };
};
class TypedEmitter<Events extends Record<string, unknown>> {
private listeners: {
[K in keyof Events]?: Array<(data: Events[K]) => void>;
} = {};
on<K extends keyof Events>(
event: K,
listener: (data: Events[K]) => void
): this {
(this.listeners[event] ??= []).push(listener);
return this;
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners[event]?.forEach((l) => l(data));
}
}
const emitter = new TypedEmitter<EventMap>();
// ✅ TypeScript knows the exact shape of each event's data
emitter.on("login", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.emit("login", { userId: "u_123", timestamp: new Date() });
// emitter.emit("login", { userId: "u_123" }); // ❌ missing 'timestamp'Validated API Response Parser
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
async function fetchJson<T>(
url: string,
validator: (data: unknown) => asserts data is T
): Promise<ApiResponse<T>> {
try {
const res = await fetch(url);
const json = await res.json();
validator(json);
return { success: true, data: json };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Unknown error",
};
}
}Deep Readonly
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type AppState = DeepReadonly<{
user: { name: string; prefs: { theme: string }[] };
}>;
declare const state: AppState;
// state.user.name = "x"; // ❌ readonly
// state.user.prefs.push({ theme: "dark" }); // ❌ readonly arrayCommon Mistakes
1. Unnecessary generics — Don't add type parameters that aren't used in at least two positions (input and output). A function function log<T>(x: T): void has no reason to be generic; function log(x: unknown): void is cleaner.
2. Overusing any in constraints — T extends any[] is fine, but T extends any is the same as no constraint at all.
3. Not leveraging inference — TypeScript can infer incredibly complex types. Write interfaces that let inference flow naturally rather than manually annotating every intermediate type.
Conclusion
Generics are the mechanism that lets TypeScript scale from simple scripts to large, refactorable codebases. Master constraints, conditional types, infer, and mapped types, and you'll write APIs that are both flexible and airtight. The patterns covered here appear constantly in popular libraries like React, Zod, and Prisma — understanding them makes reading and contributing to open source dramatically easier.
Written by
M. YousufFull-Stack Developer learning ML, DL & Agentic AI. Student at GIAIC, building production-ready applications with Next.js, FastAPI, and modern AI tools.