Why You WANT TypeScript to Break Your Build

Why You WANT TypeScript to Break Your Build

TypeScript has transformed the JavaScript ecosystem, offering type safety to a language notorious for its dynamic nature. But there's a common anti-pattern I see in many projects: developers treating TypeScript errors as mere suggestions, allowing code to compile despite glaring type issues.

In this article, I'll explain why you should want TypeScript to break your build, and how to configure it to do exactly that.

The Problem: TypeScript as a Glorified Linter

Many teams use TypeScript in a way that negates its primary benefit: catching type errors before runtime. If your TypeScript setup allows code with type errors to compile successfully, you're essentially using TypeScript as an expensive linter.

Consider this common scenario:

// TypeScript flags this as an error...
const user = getUserFromDatabase();
console.log(user.preferences.theme); // Potential null reference

// But your build still competes and ships to production!

If your build process ignores TypeScript errors, this potential null reference exception will make it to production. The whole point of TypeScript is to prevent exactly this kind of problem.

Why You Should Embrace Build Failures

When TypeScript breaks your build, it's not being mean - it's doing its job. Here are compelling reasons to embrace build failures:

1. Errors Caught Early Cost Less

Finding errors during development is significantly cheaper than catching them in production:

  • No customer impact
  • No emergency hotfixes
  • No late-night debugging sessions
  • No reputation damage

2. It Forces Resolution of Type Issues

When the build breaks, developers must address type issues immediately. This creates a virtuous cycle:

  • Types become more accurate over time
  • The codebase becomes more self-documenting
  • New team members can understand constraints more easily
  • Refactoring becomes safer

3. It Encourages Better Design

When TypeScript is strict, it nudges you toward better design patterns:

  • Properly handling null/undefined values
  • Defining clear interfaces between components
  • Being explicit about what can fail and how
  • Modeling your domain more precisely

How to Configure TypeScript to Break Your Build

Let's make TypeScript work for you by ensuring it breaks your build when it detects problems.

Step 1: Enable Strict Mode

The first step is to enable TypeScript's strict mode in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true
  }
}

This single flag enables a powerful set of type checks:

  • noImplicitAny: Disallow implicit any types
  • strictNullChecks: Make null and undefined handling explicit
  • strictFunctionTypes: Enable more accurate function parameter type checking
  • strictBindCallApply: Check the arguments of bind, call, and apply methods
  • strictPropertyInitialization: Ensure class properties are initialized
  • noImplicitThis: Flag when this has an implicit any type
  • alwaysStrict: Parse in strict mode and emit "use strict" in JS files

Step 2: Configure Additional Checks

Beyond strict mode, consider these additional compiler options:

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true
  }
}

The last option, noEmitOnError, is crucial - it prevents TypeScript from generating JavaScript output when there are errors, effectively breaking your build.

Step 3: Integrate with Your Build Process

Ensure TypeScript checking happens as part of your build process:

For npm scripts:

{
  "scripts": {
    "build": "tsc && your-bundler",
    "type-check": "tsc --noEmit"
  }
}

For Next.js (like this project):

// next.config.js or next.config.mjs
const nextConfig = {
  typescript: {
    // !! WARN !!
    // This setting breaks production builds when there are type errors
    // !! WARN !!
    ignoreBuildErrors: false,
  },
};

For Continuous Integration:

Add a dedicated type-checking step in your CI pipeline:

# Example GitHub Actions workflow
jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm install
      - run: npm run type-check

Real-world Examples of Type Errors Preventing Bugs

Let's look at how strict TypeScript can save you from common bugs:

Example 1: Null Reference Errors

// With strictNullChecks: false (bad)
function getUserName(userId: string) {
  const user = getUser(userId);
  return user.name; // Potential runtime error if user is null!
}

// With strictNullChecks: true (good)
function getUserName(userId: string) {
  const user = getUser(userId);
  if (!user) {
    return "Unknown user";
  }
  return user.name; // Safe!
}

Example 2: Unhandled Edge Cases

// Without noImplicitReturns (bad)
function getStatus(code: number): string {
  if (code === 200) {
    return "OK";
  } else if (code === 404) {
    return "Not Found";
  }
  // Oops! No return for other cases - TypeScript will flag this with noImplicitReturns
}

// With noImplicitReturns (good)
function getStatus(code: number): string {
  if (code === 200) {
    return "OK";
  } else if (code === 404) {
    return "Not Found";
  }
  return "Unknown Status"; // All paths return a value
}

Example 3: Type Inconsistencies

// Without strict mode (bad)
function calculateTotal(items) {
  // implicit any!
  return items.reduce((sum, item) => sum + item.price, 0);
}

// With strict mode (good)
interface Item {
  price: number;
  name: string;
}

function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

Handling Legacy Codebases

If you're working with a legacy codebase, enabling strict TypeScript might flag hundreds or thousands of errors. Here's how to handle this situation:

  1. Incremental Adoption: Enable strict mode but use // @ts-expect-error or // @ts-ignore comments for existing issues
  2. File-by-file Migration: Use "files" or "include" in your tsconfig to gradually add files to strict checking
  3. Set Targets: Allocate time in each sprint to fix a certain number of TypeScript errors
  4. New Code Rule: Require all new code to be strictly typed, even if legacy code isn't

Conclusion

TypeScript's true value comes from preventing type-related bugs before they reach production. By configuring TypeScript to break your build on errors, you're not creating unnecessary friction - you're establishing a vital safety net.

Remember: TypeScript errors aren't annoying warnings to be silenced. They're valuable insights alerting you to potential runtime issues. When TypeScript breaks your build, it's saving you from much bigger problems down the line.

Next time you encounter a TypeScript error, don't look for ways to silence it. Instead, fix the underlying issue and thank the type system for catching what could have been a production bug.

The small inconvenience of fixing type errors during development is nothing compared to the cost of fixing bugs in production. Embrace the breaks!