languages
May 12, 2026 · 7 min read · 0 views

TypeScript 5.7: Exhaustiveness Checking and Module Resolution Improvements

TypeScript 5.7 introduces enhanced exhaustiveness checking for union types and improved module resolution. Learn how to leverage these features to catch more bugs at compile-time.

Introduction

TypeScript 5.7, released in November 2024, brings significant improvements to type checking and module resolution that directly impact how developers catch bugs early and manage complex type hierarchies. The release focuses on exhaustiveness checking—ensuring all union type cases are handled—and refinements to how TypeScript resolves modules in both ESM and CommonJS environments.

For teams managing large codebases or working with discriminated unions, these features can eliminate entire categories of runtime errors before code reaches production. This deep-dive covers the practical implications and how to adopt these patterns in your projects today.

What’s New in TypeScript 5.7

Enhanced Exhaustiveness Checking

Exhaustiveness checking is a TypeScript feature that verifies you’ve handled all cases in a union type. In 5.7, this checking became stricter and more intelligent, especially for discriminated unions—patterns where a union type is narrowed by checking a specific property.

Consider a common scenario: an API response handler:

type ApiResponse =
  | { status: 'success'; data: unknown }
  | { status: 'error'; error: string }
  | { status: 'loading' };

function handleResponse(response: ApiResponse): string {
  if (response.status === 'success') {
    return `Data: ${JSON.stringify(response.data)}`;
  } else if (response.status === 'error') {
    return `Error: ${response.error}`;
  }
  // Missing case: 'loading'
  return 'Done'; // TypeScript 5.7 catches this!
}

In earlier versions, TypeScript would not reliably warn about the missing 'loading' case. With 5.7, the compiler is more aggressive, surfacing a type narrowing error:

const _exhaustiveCheck: never = response; // Error: Type 'ApiResponse' is not assignable to type 'never'

This pattern is now the standard way to enforce exhaustiveness. Add it at the end of your switch or if-else chain:

type Status = 'pending' | 'approved' | 'rejected' | 'archived';

function processStatus(status: Status): void {
  switch (status) {
    case 'pending':
      console.log('Waiting...');
      break;
    case 'approved':
      console.log('Approved!');
      break;
    case 'rejected':
      console.log('Denied.');
      break;
    case 'archived':
      console.log('Archived.');
      break;
    default:
      const _exhaustiveCheck: never = status;
      return _exhaustiveCheck;
  }
}

If you add a new status to the union and forget to update this function, TypeScript immediately flags it. This is invaluable for distributed teams where multiple people touch the same type definitions.

Improved Module Resolution

Module resolution is how TypeScript finds imports. In 5.7, several refinements make it easier to work with modern JavaScript packages:

Conditional Exports in package.json:

Many packages now export different code based on the environment (ESM, CommonJS, browser, etc.). TypeScript 5.7 handles these conditions more predictably:

{
  "name": "my-utils",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "types": "./dist/index.d.ts"
    },
    "./package.json": "./package.json"
  }
}

When you import from my-utils in an ESM project, TypeScript now correctly selects the import export. In a CommonJS project, it picks require. This prevents import resolution errors that plagued earlier versions.

Resolution Tracing:

Use the --explainFiles flag to debug why TypeScript chose a particular module:

tsc --explainFiles

This outputs detailed logs showing which files were checked and why certain modules were selected. Invaluable when troubleshooting “module not found” errors in monorepos.

Step-by-Step Guide: Migrating to TypeScript 5.7

Step 1: Update TypeScript

npm install -D typescript@latest

Or if using Yarn:

yarn add -D typescript@latest

Verify the installation:

npx tsc --version

Step 2: Update tsconfig.json

Ensure your tsconfig.json targets at least ES2020 and uses a modern module resolution strategy:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "bundler",
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

Step 3: Run Type Checking

npx tsc --noEmit

This checks for type errors without emitting JavaScript. You’ll likely see new exhaustiveness errors in discriminated union handlers. These are good—they represent bugs your old version of TypeScript missed.

Step 4: Fix Exhaustiveness Errors

For each error, add the missing case or update your type guard:

// Before (TypeScript 5.6)
function render(element: ReactElement | null): ReactNode {
  if (element === null) return null;
  if (React.isValidElement(element)) return element;
  // Incomplete: what about fragments, strings, numbers, arrays?
}

// After (TypeScript 5.7)
type RenderableElement = ReactElement | null | string | number | ReactElement[];

function render(element: RenderableElement): ReactNode {
  if (element === null) return null;
  if (typeof element === 'string') return element;
  if (typeof element === 'number') return String(element);
  if (Array.isArray(element)) return element.map(render);
  if (React.isValidElement(element)) return element;
  const _exhaustiveCheck: never = element;
  return _exhaustiveCheck;
}

Step 5: Test Module Resolution

Create a test file to verify imports resolve correctly:

// test-imports.ts
import { myFunction } from 'my-utils'; // Should resolve to correct export
import { z } from 'zod'; // Should find type definitions
import pkg from 'package-name'; // Should work in CommonJS mode

console.log(typeof myFunction, typeof z, typeof pkg);

Run this through your build process to confirm everything resolves:

npx tsc test-imports.ts --noEmit

Common Pitfalls and How to Avoid Them

Pitfall 1: Over-Broad Union Types

Problem: Using string | number when your type should be more specific.

// Bad: too broad
type Value = string | number | boolean | null | undefined;

function process(value: Value): void {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  }
  // Missing cases! TypeScript can't help exhaustiveness-check a 5-case union
}

// Good: discriminated union
type StatusValue =
  | { kind: 'string'; value: string }
  | { kind: 'number'; value: number }
  | { kind: 'boolean'; value: boolean };

function process(value: StatusValue): void {
  switch (value.kind) {
    case 'string':
      console.log(value.value.toUpperCase());
      break;
    case 'number':
      console.log(value.value.toFixed(2));
      break;
    case 'boolean':
      console.log(value.value ? 'true' : 'false');
      break;
  }
}

Pitfall 2: Ignoring Module Resolution Warnings

Problem: Mixing require() and import in the same project without clear rules.

// Bad: inconsistent
const express = require('express'); // CommonJS
import React from 'react'; // ESM

// Good: pick one and stick to it
import express from 'express';
import React from 'react';

If you must use both, configure TypeScript explicitly:

{
  "compilerOptions": {
    "module": "ES2020",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}

Pitfall 3: Forgetting Type Definitions for External Packages

Problem: Importing a library without types causes module resolution to fail silently.

// If @types/lodash isn't installed:
import _ from 'lodash'; // TypeScript: "Could not find declaration file"

Solution: Check if types are available and install them:

npm install --save-dev @types/lodash

Or verify the package exports its own types in package.json:

{
  "types": "./dist/index.d.ts"
}

Practical Example: Building a Type-Safe Request Handler

Let’s combine exhaustiveness checking and module resolution in a real-world scenario:

// handlers.ts
type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

type RequestMetadata = {
  method: RequestMethod;
  path: string;
  authenticated: boolean;
};

function handleRequest(metadata: RequestMetadata): { statusCode: number; body: string } {
  switch (metadata.method) {
    case 'GET':
      return { statusCode: 200, body: 'OK' };
    case 'POST':
      return { statusCode: 201, body: 'Created' };
    case 'PUT':
      return { statusCode: 200, body: 'Updated' };
    case 'DELETE':
      return { statusCode: 204, body: '' };
    case 'PATCH':
      return { statusCode: 200, body: 'Patched' };
    default:
      // If you add a new method to RequestMethod, this will error:
      const _exhaustiveCheck: never = metadata.method;
      throw new Error(`Unhandled method: ${_exhaustiveCheck}`);
  }
}

Now, if a teammate adds a new method without updating the handler, TypeScript catches it immediately.

Why It Matters

Exhaustiveness checking and improved module resolution solve real problems:

  1. Fewer Production Bugs: Catching missing cases at compile-time prevents runtime crashes.
  2. Better Refactoring: When changing type definitions, TypeScript shows every place that needs updates.
  3. Cleaner Imports: Correct module resolution eliminates subtle bugs caused by loading the wrong version of a dependency.
  4. Team Scalability: New team members can’t accidentally miss cases in a switch statement.

For large teams and mission-critical applications, these features justify upgrading to 5.7 immediately.

Testing Your Implementation

Use the Diff Checker to compare your old and new type definitions before and after migration:

# Generate a report of changes
git diff HEAD src/**/*.ts > changes.diff

If you’re working with API types, use the JSON Schema Generator to ensure your TypeScript types match your backend contracts:

// User response type
type User = {
  id: string;
  email: string;
  createdAt: string;
};

Export this as JSON Schema and validate it against your API responses.

Next Steps

  1. Update to TypeScript 5.7 in a feature branch
  2. Run tsc --noEmit to identify exhaustiveness errors
  3. Fix errors incrementally, starting with the most critical code paths
  4. Add tests that verify union types are exhaustively handled
  5. Document your discriminated union patterns for your team

TypeScript 5.7 represents a meaningful step forward in type safety. The enhanced exhaustiveness checking alone could prevent hours of debugging in production. Start small, test thoroughly, and enjoy more reliable code.

This post was generated with AI assistance and reviewed for accuracy.