TypeScript Utility Types You Didn’t Know You Could Leverage for Error Handling

Discover how TypeScript’s built-in utility types can simplify and improve error handling in your applications with practical examples for beginners.

Error handling is a crucial part of writing robust TypeScript applications. While many developers focus on try-catch blocks or custom error classes, TypeScript offers several powerful utility types that can help you handle errors more effectively and elegantly at the type level. In this article, we'll explore some of these utility types and show you practical ways to leverage them.

One of the most common challenges is representing a function that can either return a valid result or an error object. Traditionally, this is done using union types like `T | Error`. TypeScript utility types can improve this pattern, ensuring safer and clearer handling of success and error types.

Let's start by exploring how the `ReturnType` and `Exclude` utility types can help in error handling.

`ReturnType` extracts the return type of a function, which can be useful when you want to handle different outcomes of that function. Suppose you have a function that returns either a `User` object or a `CustomError` type:

typescript
type User = {
  id: number;
  name: string;
};

type CustomError = {
  message: string;
  code: number;
};

function fetchUser(id: number): User | CustomError {
  if (id === 1) {
    return { id: 1, name: "Alice" };
  } else {
    return { message: "User not found", code: 404 };
  }
}

We can capture the return type using `ReturnType` and use `Exclude` to narrow down to errors or success types.

typescript
type FetchUserReturn = ReturnType<typeof fetchUser>;

// To get the success type (User) only:
type Success = Exclude<FetchUserReturn, CustomError>;

// To get the error type only:
type Failure = Extract<FetchUserReturn, CustomError>;

// Usage example
function handleResult(result: FetchUserReturn) {
  if ("message" in result) {
    // result is inferred as CustomError
    console.error(`Error: ${result.message} (code: ${result.code})`);
  } else {
    // result is inferred as User
    console.log(`User fetched: ${result.name}`);
  }
}

Another handy utility type for error handling is `Partial`. It allows you to represent objects where some fields might be missing, which is useful for partial error data or recovery states.

typescript
type ErrorDetails = {
  message: string;
  code: number;
  stack?: string;
};

// Sometimes you might receive only part of the error information
const partialError: Partial<ErrorDetails> = {
  message: "Something went wrong"
};

`Required` works oppositely by making all fields required, which can be useful in validation phases where you want to ensure all error information is present.

typescript
function logCompleteError(error: Required<ErrorDetails>) {
  console.error(`Error: ${error.message}, Code: ${error.code}, Stack: ${error.stack}`);
}

Lastly, consider `Record` for structuring collections of errors by keys, for example, mapping error codes to messages dynamically.

typescript
const errorMessages: Record<number, string> = {
  404: "Not Found",
  500: "Internal Server Error",
  401: "Unauthorized"
};

function getErrorMessage(code: number): string {
  return errorMessages[code] || "Unknown error";
}

Using TypeScript’s utility types improves the safety and clarity of your error handling logic, catching mistakes early through static typing. As you grow your TypeScript skills, try combining these utilities to create robust error handling solutions that make your code more predictable and maintainable.