Type-Safe Event Emitter

mediumTypeScript

Lesson

TypeScript Generics with Mapped Types

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.

Example
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' } });
L7K extends keyof UserActions constrains K to be one of 'create', 'update', or 'delete'
L8UserActions[K] gives us the exact payload type for the action K
L26TypeScript will catch errors if payload doesn't match the action type

Key Takeaways

  • •Generic constraints with `extends keyof` let you create type-safe APIs that work with object keys
  • •Indexed access types `T[K]` automatically infer the correct value type for a given key
  • •This pattern provides runtime flexibility with compile-time safety - the best of both worlds
Loading...