If you've been using TypeScript for a while and are comfortable with the basics, it's time to explore some advanced patterns that can take your code to the next level. In this post, I'll share some TypeScript techniques I've found invaluable in large-scale applications.
Discriminated Unions for Type-Safe State Management
One of the most powerful TypeScript patterns is discriminated unions, especially useful for modeling state machines and Redux-like state management:
// Define a discriminated union for API request states
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// Example usage in a React component
function UserProfile() {
const [userState, setUserState] = useState<RequestState<User>>({ status: 'idle' });
useEffect(() => {
setUserState({ status: 'loading' });
fetchUser(userId)
.then(data => setUserState({ status: 'success', data }))
.catch(error => setUserState({ status: 'error', error }));
}, [userId]);
// TypeScript will ensure you handle all possible states
switch (userState.status) {
case 'idle':
return <button onClick={fetchUser}>Load Profile</button>;
case 'loading':
return <Spinner />;
case 'success':
return <ProfileDisplay user={userState.data} />;
case 'error':
return <ErrorMessage message={userState.error.message} />;
}
}
This pattern ensures you can't forget to handle a state, and property access is type-safe.
Utility Types You Should Be Using
TypeScript comes with powerful utility types that can save you time and improve type safety:
// Extract specific properties from an interface
type UserProfileFields = Pick<User, 'name' | 'email' | 'avatar'>;
// Create a read-only version of an interface
type ImmutableUser = Readonly<User>;
// Make all properties optional (great for partial updates)
type PartialUser = Partial<User>;
// Extract the return type of a function
function fetchUserData() {
// complex logic
return { id: 1, name: 'Beto' };
}
type UserData = ReturnType<typeof fetchUserData>;
Template Literal Types for API Routes
With TypeScript 4.1+, template literal types can define strongly-typed API routes:
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIEndpoint = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${APIEndpoint}`;
// TypeScript will only allow valid combinations
const validRoute: APIRoute = 'GET /users'; // Works
const invalidRoute: APIRoute = 'PATCH /users'; // Error!
Mapped Types for Consistent Transformations
Mapped types let you transform existing types in consistent ways:
// Convert all properties to functions that return that type
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface User {
id: number;
name: string;
}
// Results in: { getId: () => number, getName: () => string }
type UserGetters = Getters<User>;
Conclusion
These advanced TypeScript patterns have helped me build more robust applications with fewer runtime errors. By leveraging the type system to its fullest, you can catch errors during development rather than in production.
Remember, the goal isn't to use TypeScript features for their own sake, but to create clearer, more maintainable code. Start incorporating these patterns gradually, and you'll soon find your codebase becoming more predictable and easier to refactor.
What advanced TypeScript patterns have you found most useful? Share your experiences in the comments below!