TypeScript Generics: Write Once, Use Everywhere
Generics let you write functions and types that work across many types without sacrificing type safety. Here's everything you need.
The Problem Generics Solve
// Without generics: duplicate code or lose types
function firstString(arr: string[]): string { return arr[0]; }
function firstNumber(arr: number[]): number { return arr[0]; }
// Or use any — but lose type safety
function first(arr: any[]): any { return arr[0]; }
// With generics: one function, full type safety
function first<T>(arr: T[]): T { return arr[0]; }
const s = first(['a', 'b', 'c']); // type: string
const n = first([1, 2, 3]); // type: number
Generic Functions
// Identity function
function identity<T>(value: T): T {
return value;
}
// Pair
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
// Filter with type preservation
function filterDefined<T>(arr: (T | null | undefined)[]): T[] {
return arr.filter((x): x is T => x != null);
}
const results = filterDefined([1, null, 3, undefined, 5]);
// type: number[]
Generic Interfaces and Types
// API response wrapper
interface ApiResponse<T> {
data: T;
error: string | null;
timestamp: number;
}
type UserResponse = ApiResponse<User>;
type PostsResponse = ApiResponse<Post[]>;
// Result type (like Rust's Result)
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.user.findUnique({ where: { id } });
if (!user) return { success: false, error: new Error('Not found') };
return { success: true, data: user };
} catch (e) {
return { success: false, error: e as Error };
}
}
Constraints
// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
// T must be a key of U
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Atlas', age: 1 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'foo') // Error: 'foo' not in type
Generic Classes
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items.at(-1); }
get size(): number { return this.items.length; }
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const top = stack.peek(); // type: number | undefined
Conditional Types
// Unwrap Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<number>; // number
// Flatten array one level
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type C = Flatten<string[]>; // string
type D = Flatten<number>; // number
Utility Types (Built-in Generics)
type User = { id: string; name: string; email: string; password: string };
type PublicUser = Omit<User, 'password'>; // Remove password
type PartialUser = Partial<User>; // All optional
type RequiredUser = Required<PartialUser>; // All required
type ReadonlyUser = Readonly<User>; // No mutations
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'password'
type UserPreview = Pick<User, 'id' | 'name'>; // Only id and name
TypeScript generics are used throughout the AI SaaS Starter Kit — typed API responses, generic form hooks, and Result types. $99 at whoffagents.com.
Top comments (0)