TypeScript generics let you write reusable, type-safe code that works across multiple types without resorting to any. They are the foundation of TypeScript's type system — powering utility types, collection APIs, and library typings. Generics eliminate duplicate type declarations while preserving full compile-time safety.
What Are Generics and Why Use Them?
A generic is a type parameter — a placeholder that gets filled in when the function, class, or type is used. Instead of writing separate functions for each type, you write one function that accepts a type variable.
// Without generics — loses type information
function identity(value: any): any {
return value;
}
// With generics — preserves the exact type
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // type: number
const str = identity('hello'); // type: stringHow Do Generic Constraints Work?
Use extends to constrain a generic to types that satisfy a condition. This lets you access properties safely while keeping the function generic.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength('hello'); // OK — string has .length
logLength([1, 2, 3]); // OK — array has .length
// logLength(42); // Error — number has no .length
// Multiple constraints using intersection
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// keyof constraint — restrict to valid keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
getProperty(user, 'name'); // type: string
// getProperty(user, 'email'); // Error — 'email' is not in keyof typeof userWhich Built-in Utility Types Should You Know?
TypeScript ships with utility types that transform existing types. These are all implemented with generics internally.
| Utility | Effect | Example |
|---|---|---|
| Partial<T> | Makes all properties optional | Partial<User> |
| Required<T> | Makes all properties required | Required<Config> |
| Pick<T, K> | Selects a subset of properties | Pick<User, 'name' | 'email'> |
| Omit<T, K> | Removes specified properties | Omit<User, 'password'> |
| Record<K, V> | Creates object type with key type K and value type V | Record<string, number> |
| Readonly<T> | Makes all properties readonly | Readonly<State> |
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Update function accepts partial user data
function updateUser(id: number, data: Partial<User>): void { /* ... */ }
updateUser(1, { name: 'Bob' }); // OK — only name is required
// Public-facing type without sensitive fields
type PublicUser = Omit<User, 'password'>;
// Lookup table keyed by user ID
type UserMap = Record<number, User>;
// Form state where everything is required
type UserForm = Required<Pick<User, 'name' | 'email'>>;How Do Mapped Types Work?
Mapped types iterate over the keys of a type and transform each property. They use the in keyof syntax and are the mechanism behind Partial, Required, and Readonly.
// How Partial<T> works internally
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Prefix all keys with 'get'
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Config {
host: string;
port: number;
}
type ConfigGetters = Getters<Config>;
// { getHost: () => string; getPort: () => number }What Are Conditional Types and infer?
Conditional types choose a type based on a condition, using the T extends U ? X : Y syntax. Combined with infer, they can extract types from complex structures.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// Extract the return type of a function (how ReturnType<T> works)
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type FnReturn = MyReturnType<() => string>; // string
// Extract element type from an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Item = ElementOf<string[]>; // string
// Extract promise resolved type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Result = Awaited<Promise<Promise<number>>>; // number
// Distributive conditional types — unions are distributed automatically
type ToArray<T> = T extends unknown ? T[] : never;
type Distributed = ToArray<string | number>; // string[] | number[]Tip: Use NoInfer<T> (TypeScript 5.4+) to prevent a type parameter position from contributing to inference, forcing callers to provide the type explicitly.
Generic Patterns in Real-World Code
Generics appear everywhere in production TypeScript — API clients, state management, event emitters, and validation libraries all rely on them.
// Type-safe API client
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
const users = await fetchJson<User[]>('/api/users');
// Type-safe event emitter
type EventMap = {
login: { userId: string };
logout: undefined;
error: { message: string; code: number };
};
function on<K extends keyof EventMap>(
event: K,
handler: (payload: EventMap[K]) => void
): void { /* ... */ }
on('login', (payload) => {
console.log(payload.userId); // fully typed
});
// Builder pattern with generics
class QueryBuilder<T> {
where<K extends keyof T>(key: K, value: T[K]): this { return this; }
select<K extends keyof T>(...keys: K[]): Pick<T, K>[] { return []; }
}
new QueryBuilder<User>()
.where('name', 'Alice')
.select('name', 'email');Key Takeaways
- • Generics preserve type information across function calls — avoid
any - • Use
extendsto constrain type parameters to specific shapes - • Built-in utility types (
Partial,Pick,Omit,Record) cover most transformation needs - • Mapped types let you transform all properties of a type programmatically
- • Conditional types with
infercan extract types from complex structures - • Generic constraints with
keyofensure only valid property names are accepted