TypeScript Generics — Write Once, Work Everywhere
Generics are the mechanism that lets TypeScript be both flexible and type-safe at the same time. Instead of giving up type information to handle multiple types, generics let you describe a function or class that works with any type while preserving the specific type in context.
The Problem Generics Solve
Without generics, you face a choice: be specific (and write duplicate code) or use any (and lose safety).
// Too specific — only works for numbers
function firstNumber(arr: number[]): number {
return arr[0];
}
// Too loose — loses all type information
function firstAny(arr: any[]): any {
return arr[0];
}
const name = firstAny(['Alice', 'Bob']); // type is `any`, not `string`Generics solve this by letting you say "I don't know the type yet, but whatever it is, keep track of it":
function first<T>(arr: T[]): T {
return arr[0];
}
const name = first(['Alice', 'Bob']); // type is `string` ✓
const score = first([95, 87, 72]); // type is `number` ✓The T is a type parameter — a placeholder that TypeScript fills in at each call site.
Generic Functions
Type parameters are inferred from arguments, so you rarely need to specify them explicitly:
function wrap<T>(value: T): { value: T } {
return { value };
}
const wrapped = wrap(42); // { value: number }
const wrappedStr = wrap('hello'); // { value: string }You can also pass the type explicitly when inference isn't possible:
const empty = wrap<string[]>([]); // { value: string[] }Generic Constraints
Sometimes you need to restrict what types are allowed. Use extends to set a constraint:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength('hello'); // ✓ string has .length
getLength([1, 2, 3]); // ✓ array has .length
getLength(42); // ✗ number has no .lengthA common constraint is keyof — ensuring a key actually exists on an object:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Prabhash', age: 25 };
const name = getProperty(user, 'name'); // type is string ✓
const age = getProperty(user, 'age'); // type is number ✓
getProperty(user, 'email'); // ✗ 'email' doesn't existGeneric Interfaces
Generics work in interfaces too, which is how most data structures are typed:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: string;
name: string;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
const response = await fetchUser('123');
console.log(response.data.name); // TypeScript knows this is a stringMultiple Type Parameters
Functions can accept more than one type parameter:
function zip<A, B>(as: A[], bs: B[]): [A, B][] {
return as.map((a, i) => [a, bs[i]]);
}
const pairs = zip(['a', 'b', 'c'], [1, 2, 3]);
// type: [string, number][]Default Type Parameters
Like function defaults, you can give a type parameter a default:
interface PaginatedResult<T, Meta = Record<string, unknown>> {
items: T[];
total: number;
meta: Meta;
}
// Uses default for Meta
type UserList = PaginatedResult<User>;
// Explicit Meta type
type UserListWithCursor = PaginatedResult<User, { cursor: string }>;Built-in Generic Types
TypeScript ships with many generic types you'll use constantly:
// Promise<T> — async return types
async function loadConfig(): Promise<Config> { ... }
// Array<T> — same as T[]
const ids: Array<string> = [];
// Record<K, V> — typed object maps
const cache: Record<string, User> = {};
// Partial<T> — all fields optional
function updateUser(id: string, changes: Partial<User>) { ... }
// Required<T> — all fields required
// Readonly<T> — all fields readonly
// Pick<T, K> — subset of keys
// Omit<T, K> — all keys except KA Practical Pattern: Repository
Generics shine when building reusable infrastructure:
interface Repository<T extends { id: string }> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
}
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
private store = new Map<string, T>();
async findById(id: string) {
return this.store.get(id) ?? null;
}
async findAll() {
return [...this.store.values()];
}
async save(entity: T) {
this.store.set(entity.id, entity);
return entity;
}
async delete(id: string) {
this.store.delete(id);
}
}
// Reuse for any entity
const users = new InMemoryRepository<User>();
const posts = new InMemoryRepository<BlogPost>();Write the data layer once — TypeScript ensures each repository handles the right entity type.
In Part 4, we'll go deeper into TypeScript's type system — conditional types, mapped types, and template literal types that let you compute new types from existing ones.
TypeScript Series