Mastering TypeScript’s Advanced Type Guards for Elegant Error Handling
Learn how to use TypeScript’s advanced type guards to handle errors more effectively and write cleaner, safer code.
Handling errors gracefully is key to building robust applications. TypeScript provides powerful tools known as type guards that help you distinguish between different types at runtime, making error handling more precise and elegant. In this article, we'll explore how to master advanced type guards to improve your error handling approach.
Type guards are functions or expressions that perform runtime checks to narrow down the type of a variable within a conditional block. By using type guards, you can safely differentiate between error types or custom objects and handle them appropriately without losing TypeScript's type safety.
Let's start with a simple example. Suppose you have a function that can throw different kinds of errors, and you want to handle them differently based on their type.
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
function isNetworkError(error: unknown): error is NetworkError {
return error instanceof NetworkError;
}
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
function handleError(error: unknown) {
if (isNetworkError(error)) {
console.log('Network error occurred:', error.message);
} else if (isValidationError(error)) {
console.log('Validation failed:', error.message);
} else if (error instanceof Error) {
console.log('General error:', error.message);
} else {
console.log('Unknown error:', error);
}
}In the example above, we define two custom error classes: `NetworkError` and `ValidationError`. We then create two type guard functions, `isNetworkError` and `isValidationError`, which narrow down the `error` variable to the specific error types using the `instanceof` operator.
Calling these type guard functions within the `handleError` function allows TypeScript to understand the exact error type inside each conditional branch, enabling safer and more readable error handling.
Advanced type guards aren't limited to checking instances — you can also guard objects based on their shape or properties. Here's an example of how you might distinguish between error objects with different structures.
interface ApiError {
errorCode: number;
message: string;
}
interface DatabaseError {
dbCode: string;
description: string;
}
function isApiError(error: any): error is ApiError {
return typeof error.errorCode === 'number' && typeof error.message === 'string';
}
function isDatabaseError(error: any): error is DatabaseError {
return typeof error.dbCode === 'string' && typeof error.description === 'string';
}
function logError(error: unknown) {
if (isApiError(error)) {
console.error(`API Error (${error.errorCode}): ${error.message}`);
} else if (isDatabaseError(error)) {
console.error(`DB Error (${error.dbCode}): ${error.description}`);
} else {
console.error('Unknown error', error);
}
}This pattern works well when errors don't have a shared class hierarchy but can be differentiated by their properties. Using these guards, you can elegantly handle diverse error shapes.
In summary, mastering advanced type guards allows you to write cleaner, more maintainable error handling logic in TypeScript. You get all the benefits of static type checking combined with flexible runtime checks — leading to fewer bugs and better developer experience.
Start incorporating these patterns in your projects today to handle errors gracefully and with confidence!