3 minute read

Ever had that moment where you spend time carefully configuring TypeScript, only to discover your build process has been cheerfully ignoring most of your settings? I recently fell into this trap while working with esbuild, and the experience taught me some valuable lessons about TypeScript tooling.

The Setup

Here's how it started. I had a TypeScript project with some strict type checking rules in my tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

My build process used esbuild, configured like this:

esbuild.build({
    entryPoints: ['src/index.ts'],
    bundle: true,
    outdir: 'dist',
    format: 'esm',
    target: ['es2020'],
    loader: { '.ts': 'ts' }
});

Everything seemed fine - my code built successfully, and I was feeling good about my type safety. Until I discovered some code that should have failed type checking was making it into production.

The Gotcha

Here's what I didn't realize: esbuild, while excellent at quickly bundling TypeScript, isn't actually a TypeScript compiler. It's primarily focused on:

  1. Parsing TypeScript syntax
  2. Stripping out type annotations
  3. Transforming and bundling the remaining JavaScript

By default, esbuild doesn't read from tsconfig.json at all! Even when explicitly configured to do so using the tsconfig option, it only reads a limited set of settings:

  • baseUrl and paths for module resolution
  • target for output JavaScript version
  • jsxFactory and jsxFragmentFactory for JSX handling

What it doesn't do is enforce type checking rules. All those careful type safety configurations? They weren't being enforced during the build.

The Solution

The fix is to add explicit type checking to your build process. Here's how:

const { build } = require('esbuild');
const { exec } = require('child_process');

// First run type checking
exec('npx tsc --noEmit', (error) => {
  if (error) {
    console.error('Type checking failed:', error);
    process.exit(1);
  }

  // Then run esbuild
  build({
    // your existing esbuild config...
  });
});

Or in your package.json scripts:

{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build": "npm run type-check && esbuild ...",
    "dev": "npm run type-check && esbuild ... --watch"
  }
}

The --noEmit flag tells TypeScript to only check types without generating JavaScript files (since esbuild will handle the actual compilation).

Understanding Build Tool Roles

This experience highlighted an important distinction in the TypeScript tooling ecosystem:

  • TypeScript Compiler (tsc): Focuses on type checking and compilation
  • Build Tools (esbuild, webpack, etc.): Focus on bundling, minification, and other build optimizations

While many build tools can handle TypeScript syntax, they don't necessarily enforce TypeScript's type system. They're designed for speed and efficiency in transforming and bundling code, not for type safety.

Best Practices

To avoid this trap:

  1. Always include explicit type checking in your build process
  2. Don't assume that TypeScript-compatible build tools are enforcing your TypeScript configuration
  3. Consider running type checking in parallel with development for faster feedback
  4. Set up CI checks that run the TypeScript compiler to catch type errors before deployment

The Bigger Picture

This situation reflects a broader principle in software development: tools that seem to be doing the same thing (handling TypeScript files) might have very different focuses and trade-offs. Understanding these differences is crucial for building reliable development pipelines.

In the case of TypeScript and build tools:

  • TypeScript's compiler prioritizes type safety and correctness
  • Build tools prioritize speed and bundle optimization
  • You often need both to get the best of both worlds

Conclusion

While it might seem redundant to run both the TypeScript compiler and a build tool, this separation of concerns actually makes a lot of sense. The TypeScript compiler can focus on providing robust type checking, while build tools like esbuild can focus on creating optimal bundles.

The key is to remember that seeing ".ts" in your build configuration doesn't mean you're getting full TypeScript type checking. Make sure you're explicitly running tsc as part of your build process, and you'll catch those type errors before they make it to production.

Has this ever happened to you? How do you handle TypeScript type checking in your build pipeline? Let me know in the comments!

Updated:

Comments