Migrating from tsc to esbuild for Faster Builds
Migrate TypeScript library builds from tsc to esbuild. Decouple type-checking from transpilation, configure esbuild for dual ESM/CJS output, and measure CI build time improvements.
Pre-Migration tsconfig.json Sanitization
esbuild operates as a transpiler-only architecture and ignores TypeScript compiler directives. Passing tsc-specific flags directly triggers:
error TS5023: Unknown compiler option 'verbatimModuleSyntax' when passed to esbuild
To successfully migrate tsc to esbuild, decouple type-checking from transpilation. Strip verbatimModuleSyntax, isolatedModules, and declaration from your active configuration. Externalize baseUrl and paths resolution to the bundler’s resolver layer. For foundational compiler option decoupling strategies, reference TypeScript Configuration & Build Tooling.
Dual ESM/CJS Output Configuration
Generate parallel format artifacts using native CLI invocations. Executing mixed formats in a single pass produces:
error: Cannot use 'import' with a CommonJS output format
Run separate commands: --format=esm paired with --out-extension:.js=.mjs, and --format=cjs paired with --out-extension:.js=.cjs. Apply --platform=node for backend libraries to preserve Node.js built-ins and prevent polyfill injection. When contrasting esbuild’s AST-based transpilation speed against traditional bundler architectures for dual-format output, review Modern Build Tools: tsup, Rollup, and esbuild.
Path Alias & Monorepo Workspace Resolution
esbuild’s resolver does not parse tsconfig.json paths natively. Unmapped aliases halt compilation with:
error: Could not resolve '@internal/utils' (mark it as external to exclude it from the bundle)
Replicate mappings using --alias:@internal/utils=./packages/utils/src. Configure --external:@internal/* to prevent workspace dependencies from being inlined. Ensure package.json main/module fallbacks align with your output directories. Use --resolve-extensions=.ts,.tsx,.js,.json to bypass explicit extension requirements during resolution.
Strict Type-Checking Pipeline Separation
esbuild strips types and emits zero .d.ts artifacts. Relying on it for validation causes downstream consumers to fail with:
error TS6305: Output file has not been built from source file
Maintain zero-runtime type safety by running tsc --noEmit in CI pre-build hooks. Create a dedicated tsconfig.build.json with skipLibCheck: true to accelerate validation. Parallelize the type-checking and esbuild execution stages in your pipeline to eliminate bottlenecks without sacrificing compile-time guarantees.
package.json Exports & Dual-Package Hazard Mitigation
Consumers relying on conditional exports will fail if the manifest lacks explicit format routing:
ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath './dist/index' is not defined by 'exports'
Define exports with strict import (ESM) and require (CJS) conditions. Point types to the generated .d.ts output directory. Prevent circular resolution by marking peer dependencies as --external during the build phase.
Step-by-Step Execution
- Strip incompatible compiler flags from tsconfig.json
Remove
verbatimModuleSyntax,isolatedModules, anddeclarationfromtsconfig.json. Create a separatetsconfig.build.jsonwith:
{
"compilerOptions": {
"noEmit": true,
"skipLibCheck": true
}
}
- Configure esbuild for parallel ESM/CJS emission Execute distinct CLI commands for each target format:
npx esbuild src/index.ts --bundle --outdir=dist/esm --format=esm --out-extension:.js=.mjs --platform=node && npx esbuild src/index.ts --bundle --outdir=dist/cjs --format=cjs --out-extension:.js=.cjs --platform=node
- Generate declaration files via tsc --emitDeclarationOnly
Run the type compiler strictly for
.d.tsemission:
npx tsc --project tsconfig.build.json --emitDeclarationOnly --declarationDir dist/types
- Map conditional exports in package.json Route consumers explicitly to prevent dual-package hazards:
"exports": {
".": {
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.mjs"
},
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.cjs"
}
}
}