Mastering Advanced TypeScript Error Inference for Cleaner Code

Learn how to use TypeScript's advanced error inference to write cleaner, safer, and more maintainable code with practical examples.

TypeScript helps catch errors early by inferring types automatically. However, sometimes your code can be improved even more by mastering TypeScript's error inference capabilities. This article walks you through some advanced concepts around error inference, making your code cleaner and easier to maintain.

A core strength of TypeScript is its ability to infer types from your code. But what about inferring errors? Advanced error inference lets TypeScript catch complex bugs without you explicitly defining every detail. Let's start with a simple example.

typescript
function getUserName(user: { name?: string }) {
  // TypeScript infers name might be undefined
  if (!user.name) {
    throw new Error("User name is missing");
  }
  return user.name;
}

Here, TypeScript understands that `user.name` might be undefined because it is an optional property. The check `if (!user.name)` helps TypeScript narrow down the type safely. This prevents runtime errors by forcing a check before usage.

Now, consider a function where you want to infer errors in a more dynamic scenario with conditional types and advanced inference techniques.

typescript
type Result<T> = { success: true; value: T } | { success: false; error: string };

function parseJson<T>(json: string): Result<T> {
  try {
    const parsed = JSON.parse(json) as T;
    return { success: true, value: parsed };
  } catch (e) {
    return { success: false, error: (e as Error).message };
  }
}

const result = parseJson<{ name: string }>(`{"name":"Alice"}`);

if (!result.success) {
  // TypeScript infers this is the error case
  console.error("Parsing failed:", result.error);
} else {
  // TypeScript infers the value is correctly typed
  console.log("Parsed name:", result.value.name);
}

In this example, we define a `Result` type that can represent either success or failure. TypeScript uses this union to infer errors and success cases, enabling more precise type checking. This approach allows you to handle errors cleanly without losing type-safety.

You can also create utility types to infer errors from functions automatically. Here's a basic pattern to extract error types from promise-returning functions.

typescript
type InferError<T> = T extends (...args: any[]) => Promise<infer R>
  ? R extends { success: false; error: infer E }
    ? E
    : never
  : never;

async function fetchUser(): Promise<Result<{ id: number; name: string }>> {
  // Example fetching user
  return { success: true, value: { id: 1, name: "Alice" } };
}

// TypeScript infers the error type as string
type FetchUserError = InferError<typeof fetchUser>;

Using `InferError`, TypeScript automatically extracts the error type from the fetch function's return value, making error handling easier and more consistent across your codebase.

In summary, mastering advanced error inference in TypeScript helps you write cleaner, more maintainable, and strongly-typed error handling logic. Use union types for success/error patterns, apply conditional types to infer error types, and always leverage TypeScript's narrowing abilities for safer code.