Generic functions that infer
The best generic is one the caller never notices:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type: number | undefined
const str = first(['a', 'b']); // type: string | undefined
TypeScript infers T from the argument. The caller does not write first<number>([1, 2, 3]) — they just call the function and get correct types.
Constrained generics
When a generic needs to have certain properties, constrain it with extends:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // type: string
const age = getProperty(user, 'age'); // type: number
getProperty(user, 'email'); // Error: 'email' is not in keyof user
The constraint K extends keyof T ensures you can only access properties that exist. The return type T[K] is specific — not any, not unknown, but the exact type of that property.
Generic API response wrapper
Every API has a standard response shape. Make it generic:
type ApiResponse<T> = {
data: T;
meta: {
requestId: string;
timestamp: string;
};
};
type PaginatedResponse<T> = ApiResponse<T[]> & {
pagination: {
page: number;
pageSize: number;
total: number;
};
};
// Usage
async function getUsers(): Promise<PaginatedResponse<User>> {
// TypeScript knows the return shape exactly
}
const response = await getUsers();
response.data[0].email; // Autocomplete works
response.pagination.total; // Type-safe
Generic repository pattern
interface Repository<T extends { id: string }> {
findById(id: string): Promise<T | null>;
findMany(filter: Partial<T>): Promise<T[]>;
create(data: Omit<T, 'id'>): Promise<T>;
update(id: string, data: Partial<Omit<T, 'id'>>): Promise<T>;
delete(id: string): Promise<void>;
}
class PrismaRepository<T extends { id: string }> implements Repository<T> {
constructor(private model: any) {}
async findById(id: string): Promise<T | null> {
return this.model.findUnique({ where: { id } });
}
async create(data: Omit<T, 'id'>): Promise<T> {
return this.model.create({ data });
}
// ... other methods
}
// Type-safe for each entity
const userRepo = new PrismaRepository<User>(prisma.user);
const postRepo = new PrismaRepository<Post>(prisma.post);
Conditional types for API contracts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type RequestBody<M extends HttpMethod> =
M extends 'GET' | 'DELETE' ? never :
M extends 'POST' | 'PUT' ? Record<string, unknown> :
never;
function request<M extends HttpMethod>(
method: M,
url: string,
...args: RequestBody<M> extends never ? [] : [body: RequestBody<M>]
) {
// ...
}
request('GET', '/api/users'); // No body allowed
request('POST', '/api/users', { name: 'Alice' }); // Body required
request('GET', '/api/users', {}); // Error: too many arguments
The conditional type makes body required for POST/PUT and forbidden for GET/DELETE.
The golden rule
If a generic type parameter appears only once in the function signature, you do not need it:
// Bad: T is used once, just use the concrete type
function logValue<T>(value: T): void {
console.log(value);
}
// Good: no generic needed
function logValue(value: unknown): void {
console.log(value);
}
Generics connect types across multiple positions — input to output, argument to argument. If there is nothing to connect, use a concrete type.
