TypeScript's type system becomes incredibly powerful when you combine generics with mapped types and keyof operators. This combination allows you to create APIs that are both flexible and completely type-safe.
A generic type parameter like <T> lets you write code that works with different types while preserving type information. When you constrain a generic with extends, you're telling TypeScript what properties or structure that type must have.
Mapped types let you create new types by transforming properties of existing types. The keyof operator extracts all the keys from a type as a union. When combined, these features let you build APIs where TypeScript can infer the correct types based on the arguments you pass.
The key insight is using indexed access types like T[K] where K extends keyof T. This pattern means "given a key K that we know exists in type T, give me the type of the value at that key." TypeScript can then enforce that your function parameters and return types match exactly.
This approach is commonly used in libraries like React's event system, Redux action creators, and database ORMs. It provides the flexibility of dynamic key-value operations with the safety of compile-time type checking.
The beauty of this pattern is that TypeScript does all the heavy lifting - your runtime code can be simple and straightforward while the type system prevents entire classes of bugs.
1// Define a schema using mapped types
2type UserActions = {
3 'create': { name: string; email: string };
4 'update': { id: number; changes: Partial<{ name: string; email: string }> };
5 'delete': { id: number };
6};
7
8// Generic function that enforces type safety
9function handleAction<K extends keyof UserActions>(
10 action: K,
11 payload: UserActions[K]
12): string {
13 switch (action) {
14 case 'create':
15 // TypeScript knows payload has name and email
16 return `Creating user: ${(payload as UserActions['create']).name}`;
17 case 'update':
18 // TypeScript knows payload has id and changes
19 return `Updating user: ${(payload as UserActions['update']).id}`;
20 case 'delete':
21 // TypeScript knows payload has id
22 return `Deleting user: ${(payload as UserActions['delete']).id}`;
23 default:
24 return 'Unknown action';
25 }
26}
27
28// Usage with full type safety
29handleAction('create', { name: 'John', email: 'john@example.com' });
30handleAction('update', { id: 1, changes: { name: 'Jane' } });