Mastering TypeScript: Advanced Patterns

Moving beyond basic type annotations is where TypeScript truly shines. To build robust, scalable applications, you need to leverage the type system to enforce business logic at compile-time. Here are three advanced patterns to sharpen your TypeScript skills.
1. Discriminated Unions for State Management
Instead of using optional properties or multiple flags, use a “tag” (literal type) to create a Discriminated Union. This forces your code to handle every possible state explicitly, preventing “impossible states.”
type AppState =
| { status: 'loading' }
| { status: 'success', data: string }
| { status: 'error', error: Error };
function handle(state: AppState) {
switch (state.status) {
case 'success': console.log(state.data); break;
case 'error': console.error(state.error); break;
}
}
2. Mapped Types and Template Literal Types
Mapped types allow you to transform existing types into new ones, while Template Literal types let you manipulate string types dynamically. These are incredibly powerful for creating type-safe utility functions.
- Mapped Types: Useful for creating partial versions of objects or record-based utilities.
- Template Literals: Perfect for defining event names or CSS-in-JS properties (e.g.,
'on' + Capitalize<T>).
type Keys = 'firstName' | 'lastName';
type UserState = { [K in Keys]: boolean };
// Result: { firstName: boolean; lastName: boolean; }
3. Conditional Types and Infer
Conditional types act as an if statement for your types. Combined with the infer keyword, you can “extract” types from functions, promises, or arrays.
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Usage:
type Result = GetReturnType<() => number>; // Result is 'number'
Pro-Tip: The Utility Belt
Don’t reinvent the wheel. Before writing complex generic logic, check if the built-in TypeScript utility types—like Pick<T, K>, Omit<T, K>, Required<T>, and ReturnType<T>—already cover your needs.