TypeScript Course EP11: Generics in Practice
Video
Watch the full video on YouTube:
Introduction
In this eleventh chapter of our TypeScript series, we dive deep into generics in practice with three real-world use cases. We’ll create a generic API Response, implement a custom Pick function, and build a typed custom event emitter with advanced static typing.
Generics can be one of the most complex topics to understand at first, but once you master them, they allow you to create reusable, flexible code with a robust type system that catches errors at compile time.
Generic API Response
The first use case is standardizing the format of a backend’s response. We’ll create an interface that receives two generics T and U to separate the result from the metadata.
API Response Interface
export interface ApiResponse<T, U> {
result: T;
metadata: U;
}
Here we use two generics because a common practice is to separate the result from the metadata we might send.
Class Implementation
interface User {
id: number;
name: string;
email: string;
password: string;
}
export class UserResponseDto implements ApiResponse<User, null> {
result: User;
metadata: null;
constructor(user: User) {
this.result = user;
this.metadata = null;
}
}
Implicit Assignment with Constructor
TypeScript offers a syntax to assign values automatically without redundant code:
export class UserResponseDto implements ApiResponse<User, null> {
constructor(public result: User, public metadata: null = null) {}
}
This approach is cleaner and avoids code duplication. TypeScript creates the attributes and assigns them automatically.
Custom Pick Function
The second use case is creating a function that extracts specific attributes from an object, similar to the Pick utility type, but on the JavaScript side (runtime).
Function Definition
export function pick<T extends Record<string, unknown>, U extends keyof T>(
origin: T,
keys: U[]
): Pick<T, U> {
const result = {} as Pick<T, U>;
keys.forEach((key) => {
result[key] = origin[key];
});
return result;
}
Generics Explained
T extends Record<string, unknown>: ConstrainsTto be a dictionary-like objectU extends keyof T: ConstrainsUto be valid keys ofT- The return type is
Pick<T, U>: A new type with only the selected attributes
Usage Example
type User = {
id: number;
name: string;
email: string;
password: string;
};
const user2: User = {
id: 1,
name: "Angel",
email: "angel@gmail.com",
password: "password123"
};
const publicUser = pick(user2, ["id", "name", "email"]);
console.log(user2);
// { id: 1, name: "Angel", email: "angel@gmail.com", password: "password123" }
console.log(publicUser);
// { id: 1, name: "Angel", email: "angel@gmail.com" }
The function extracts only the attributes we specify, creating a new object without the password.
Typed Custom Event Emitter
The third use case is more advanced. We’ll create an event emitter with static typing that validates both the event name and the payload’s data type.
Event Map
type EventsMap = {
"user:created": { id: number; email: string };
"user:updated": { id: number; name?: string; email?: string };
};
Custom Event Emitter Class
export class CustomEventEmitter<T extends Record<string, unknown>> {
emit<K extends keyof T>(event: K, input: T[K]) {
console.log(`Event: ${String(event)}`, JSON.stringify(input, null, 4));
}
}
Using the Emitter
const emitter = new CustomEventEmitter<EventsMap>();
// TypeScript knows "user:created" requires id and email
emitter.emit("user:created", { id: 1, email: "angel@gmail.com" });
// TypeScript knows "user:updated" requires id, name and email are optional
emitter.emit("user:updated", { id: 2, name: "Angel 2", email: "angel2@gmail.com" });
How the Typing Works
T extends Record<string, unknown>: Constrains the generic to a dictionaryK extends keyof T: Each event is a valid key of the dictionaryinput: T[K]: The input type is the type of the value at that key
TypeScript statically validates that each event receives the correct data type. If you try to pass an incorrect type, the compiler throws an error.
Best Practices
- Use generics to create reusable interfaces like
ApiResponse - Constrain generics with
extendsto restrict what types can be passed - Leverage implicit constructor assignment to avoid redundant code
- Create runtime utility types that complement TypeScript’s built-in ones
- Use
keyofandtypeofto make functions that operate on existing types
Conclusion
Generics in practice allow you to create robust, reusable code. From standardized API Responses to typed event emitters, generics are a fundamental tool for any TypeScript developer who wants to write maintainable and scalable code.
Repository
💻 Repository: https://github.com/radagva/ts-example
Next Steps
🔔 Subscribe to the channel to not miss the next chapters of the TypeScript course!
