Beyond Simple Type Parameters
Most developers encounter TypeScript generics in basic collection wrappers like Array<T> or Promise<T>. However, when building scalable domain boundaries and reusable libraries, generics unlock powerful compile-time validations that prevent entire classes of runtime defects.
Conditional Types and Type Distribution
Conditional types allow you to declare relationships where the resulting type depends on a type condition, mirroring ternary expressions in standard logic:
// Extract non-nullable function return types
type NonEmptyResult = T extends (...args: any[]) => infer R
? R extends null | undefined
? never
: R
: never;
// Example usage
type ApiHandler = () => string | null;
type SafeOutput = NonEmptyResult; // resolves to string
Type-Safe Event Emitter Pattern
A classic application is typing event names and their corresponding payloads without manual type casting:
type EventMap = {
'user:signup': { userId: string; email: string; timestamp: number };
'order:created': { orderId: string; totalAmount: number };
'system:alert': { level: 'info' | 'warn' | 'error'; message: string };
};
class TypedEventEmitter> {
private listeners: { [K in keyof TEvents]?: Array<(payload: TEvents[K]) => void> } = {};
on(event: K, handler: (payload: TEvents[K]) => void): void {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event]!.push(handler);
}
emit(event: K, payload: TEvents[K]): void {
this.listeners[event]?.forEach((fn) => fn(payload));
}
}
Conclusion
Investing in precise generic boundaries guarantees self-documenting code and provides IDE autocomplete that saves development cycles across engineering teams.