TypeScript 6.0 `--noEmit` and Type-Only Builds: Why Your CI Pipeline Should Never Call tsc for Output Again

TypeScript 6.0 --noEmit and Type-Only Builds: Why Your CI Pipeline Should Never Call tsc for Output Again This article was written with the assistance of AI, under human supervision and review. Most TypeScript build problems stem from a fundamental confusion: teams treat the TypeScript compiler as both a type-checker and a build tool when it should only ever be the former. The typical CI pipeline runs tsc to transpile TypeScript into JavaScript, then bundles that output with webpack or Rollup. T
TypeScript 6.0 --noEmit and Type-Only Builds: Why Your CI Pipeline Should Never Call tsc for Output Again
This article was written with the assistance of AI, under human supervision and review.
Most TypeScript build problems stem from a fundamental confusion: teams treat the TypeScript compiler as both a type-checker and a build tool when it should only ever be the former. The typical CI pipeline runs tsc to transpile TypeScript into JavaScript, then bundles that output with webpack or Rollup. This sequential flow burns 2-4 minutes per deploy while the type-checker blocks faster tools from doing their job.
The --noEmit flag separates type-checking from code generation. When configured correctly, your pipeline runs type validation in parallel with a dedicated transpiler like esbuild or swc. The result is 60-80% faster builds with identical type safety. The failure mode here is subtle but expensive: every second your CI spends waiting for tsc to emit files is a second your deployment sits in a queue.
TypeScript 6.0's native execution model changes this equation entirely. The compiler no longer needs to produce intermediate JavaScript for local development, and modern bundlers strip types directly from .ts files. This means tsc can focus exclusively on what it does best: validating types. Production builds never touch the TypeScript compiler's output logic.
That covers why type-only builds matter. The sections below show exactly how to implement this pattern in production CI pipelines, compare the tooling landscape, and migrate legacy build scripts without breaking existing workflows.
Key Takeaways
- The
--noEmitflag makes tsc a pure type-checker that never writes files, enabling parallel builds with faster transpilers. - Modern bundlers (esbuild, swc) strip TypeScript syntax 10-20x faster than tsc transpiles, but cannot validate types.
- TypeScript 6.0's native execution removes the need for tsc output in development entirely—only CI type-checking remains.
- A correctly configured pipeline runs
tsc --noEmitin parallel with your bundler, catching type errors without blocking the build. - Declaration files (
.d.ts) still require tsc for libraries, but application code should never emit them.
Understanding --noEmit: Type-Checking Without Code Generation
The --noEmit compiler option tells TypeScript to perform its full type analysis without writing any output files. This distinction is critical. When you run tsc without this flag, the compiler does two separate jobs: it validates types and then transpiles your code into JavaScript. These operations happen sequentially even though they have zero logical dependency on each other.
// tsconfig.json
{
"compilerOptions": {
"noEmit": true,
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
Enter fullscreen mode Exit fullscreen mode
With this configuration, running tsc exits immediately after type-checking. The compiler still reads every file, resolves imports, checks assignability, and reports errors. It simply skips the code generation phase. This shaves 40-60% off execution time because AST traversal for type validation is significantly faster than transformation and file I/O.
The implication here is that your build tool—esbuild, webpack, Rollup, or swc—can start transpiling source files the moment CI begins. It does not wait for TypeScript to finish. Both processes run concurrently, and your build completes when the slower of the two finishes. In practice, transpilation always wins because modern bundlers skip type-checking entirely.
The configuration also enables TypeScript 6.0's moduleResolution: "bundler" mode, which assumes your bundler handles import resolution. This removes the mismatch between what TypeScript expects and what tools like webpack actually do. Developers no longer see false errors about missing extensions or incompatible module formats.
One caveat applies to library authors: if you publish a package to npm, you need declaration files (.d.ts). These require tsc to emit them because no other tool generates TypeScript's type definitions. For libraries, use a separate tsconfig.build.json with "noEmit": false and "emitDeclarationOnly": true. Application code should never touch this configuration.
Setting Up Type-Only Checks in CI/CD
A production CI pipeline needs two parallel jobs: one runs tsc --noEmit to validate types, the other runs your bundler to produce deployable artifacts. Both jobs must pass before the pipeline succeeds. This pattern ensures type errors block deployment without slowing down the build itself.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run typecheck
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
Enter fullscreen mode Exit fullscreen mode
The typecheck script in package.json runs tsc --noEmit. The build script runs your bundler with TypeScript stripped but no type validation. Both jobs install dependencies independently, but npm's cache makes this negligible. The critical detail is that they execute simultaneously—GitHub Actions schedules them in parallel by default when they have no dependencies.
// package.json
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "esbuild src/index.ts --bundle --outdir=dist --platform=node",
"dev": "esbuild src/index.ts --bundle --outdir=dist --watch"
}
}
Enter fullscreen mode Exit fullscreen mode
This matters because the typechecking job typically finishes first (2-3 minutes) while the build job handles bundling, minification, and asset optimization (1-2 minutes). If types fail, the entire pipeline fails immediately without waiting for the build to complete. If types pass but the build fails, you see bundler errors instead of cryptic type mismatches. The error messages stay contextual.
For monorepos, run tsc --noEmit once at the root with project references configured. Do not run separate type-checks per package—TypeScript already validates the entire workspace in a single pass. The composite project feature resolves cross-package imports without emitting intermediate build artifacts.
One common mistake is running tsc inside the build script itself. Teams do this because legacy tooling required it, but modern bundlers handle TypeScript natively. Your build script should invoke webpack, esbuild, or swc directly. The typechecking happens elsewhere.
tsc vs esbuild vs swc: Which Tool Does What
Three tools dominate TypeScript compilation in 2026, and each serves a distinct purpose. Understanding their tradeoffs determines whether your build is fast, correct, or both.
The TypeScript compiler (tsc) performs full semantic analysis. It resolves every type, checks assignability across module boundaries, validates generic constraints, and reports errors with precise line numbers. This process requires building a complete type graph of your codebase. Transpilation happens as a side effect after type-checking completes, and it preserves your source's structure almost exactly. The output is readable but slow to produce.
esbuild is a Go-based bundler that strips TypeScript syntax without validating types. It parses your code into an AST, removes type annotations, transforms modern JavaScript to your target, and bundles everything in a single pass. The result is 10-50x faster than tsc for transpilation, but you get zero type safety. If you pass invalid TypeScript to esbuild, it produces broken JavaScript without warning.
swc (Speedy Web Compiler) is a Rust-based alternative to esbuild with similar performance characteristics. It also strips types without checking them. The advantage over esbuild is better source map quality and more configurable output, but the core limitation is identical: no type validation. Both tools assume you run tsc --noEmit separately.
The implication here is that you need both a type-checker and a transpiler. You cannot choose one over the other. Teams that skip type-checking because esbuild is faster ship runtime crashes. Teams that use only tsc wait 4-6 minutes for builds that should take 90 seconds.
One exception applies: if you use Babel with @babel/preset-typescript, you get the worst of both worlds. Babel strips types slowly and does not check them. The only reason to use Babel in 2026 is for custom syntax transformations that esbuild and swc do not support. Even then, run it after esbuild for performance.
The correct pattern is tsc --noEmit for type safety plus esbuild or swc for output. This combination gives you sub-2-minute CI builds with full type coverage. The next section shows exactly how to wire this up in production.
Production CI Pipeline Pattern: Parallel Type-Checking and Building
A production-grade pipeline runs type-checking and building as independent jobs that must both succeed. The key insight is that neither job depends on the other's output—they read the same source files and produce different artifacts.
Here is a complete GitHub Actions configuration that implements this pattern with caching and failure handling:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci --prefer-offline
- run: npm run typecheck
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci --prefer-offline
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: [typecheck, build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: |
# Deploy dist/ to your hosting provider
echo "Deploying to production"
Enter fullscreen mode Exit fullscreen mode
The needs: [typecheck, build] directive in the deploy job ensures both checks pass before deployment runs. If types fail, the entire pipeline stops. If the build fails, deployment never triggers. The timeout prevents stuck jobs from blocking your queue indefinitely.
The cache: 'npm' option in setup-node reuses node_modules between runs. This cuts install time from 60 seconds to 5-10 seconds because npm only fetches changed packages. The --prefer-offline flag tells npm to use cached tarballs when available, which matters for private registries with rate limits.
One critical detail: the typecheck job does not upload artifacts. It only validates types and exits. The build job uploads the bundled output, which the deploy job downloads. This separation keeps artifacts small and avoids storing intermediate files that CI never uses.
For monorepos with multiple packages, run a single typecheck at the root and individual build jobs per package. TypeScript's project references handle cross-package type validation automatically:
// tsconfig.json (root)
{
"compilerOptions": {
"composite": true,
"noEmit": true
},
"references": [
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
Enter fullscreen mode Exit fullscreen mode
Running tsc --noEmit at the root validates all packages in dependency order. Each package's build script runs esbuild or swc independently. This pattern scales to 50+ packages without performance degradation because type-checking happens once and builds run in parallel.
The failure mode here is running tsc without --noEmit inside the build script. Teams do this because older tutorials suggested it, but it doubles your build time by transpiling code that esbuild already handles. The correct pattern is two separate commands: tsc --noEmit for types and esbuild for output.
TypeScript 6.0 Native Execution Changes the Game
TypeScript 6.0 introduces native execution mode, which allows running .ts files directly in Node.js without transpilation. This feature eliminates the need for ts-node, tsx, or any other runtime wrapper during development. The TypeScript compiler embeds itself into the Node.js runtime and strips types on-the-fly.
This matters because development workflows no longer need a separate build step. Developers run node --experimental-strip-types src/index.ts and the file executes immediately. Hot reload tools like nodemon watch for changes and restart without invoking tsc. The feedback loop shrinks from 3-5 seconds to under 500ms.
In other words, the TypeScript compiler's role shifts entirely to CI/CD. Local development uses native execution. Production builds use esbuild or swc. Type-checking happens in CI with tsc --noEmit. The compiler never emits files except for libraries that need declaration files.
The catch is that native execution does not validate types—it only strips them. If you write invalid TypeScript, Node.js executes the broken JavaScript and crashes at runtime. This is identical to using esbuild or swc locally. The solution is the same: run tsc --noEmit in watch mode during development.
// package.json
{
"scripts": {
"dev": "concurrently \"tsc --noEmit --watch\" \"node --watch --experimental-strip-types src/index.ts\"",
"build": "esbuild src/index.ts --bundle --outdir=dist",
"typecheck": "tsc --noEmit"
}
}
Enter fullscreen mode Exit fullscreen mode
The concurrently package runs both commands in parallel. One terminal pane shows type errors as you code. The other pane restarts your server on file changes. Both processes stay fast because they do not block each other.
One limitation applies to decorators and advanced TypeScript features: native execution uses the JavaScript engine's parser, which does not support every TypeScript syntax extension. If your codebase uses experimental features, you still need a transpiler during development. Check the Node.js compatibility matrix before adopting this pattern.
The long-term implication is that tooling complexity decreases dramatically. Teams no longer need ts-node, tsx, tsconfig-paths, or loader hooks. The runtime handles TypeScript natively. This reduces dependency count, eliminates version conflicts, and makes onboarding new developers faster.
Migration Strategy: Removing tsc from Your Build Step
Migrating from a legacy build that uses tsc for transpilation to a type-only pipeline requires three steps: extract type-checking into a separate script, replace tsc with a faster transpiler, and update your CI configuration.
Start by adding "noEmit": true to your tsconfig.json and creating a new typecheck script in package.json. Run this script locally to verify it catches the same errors as your current build. Do not change the build script yet.
// tsconfig.json (before)
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"target": "ES2020"
}
}
// tsconfig.json (after)
{
"compilerOptions": {
"noEmit": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
Enter fullscreen mode Exit fullscreen mode
The outDir and rootDir options become irrelevant when noEmit is enabled. Remove them to avoid confusion. The moduleResolution: "bundler" setting tells TypeScript to trust your bundler's import resolution instead of enforcing its own rules.
Next, replace tsc in your build script with esbuild or swc. If you currently run tsc && webpack, change it to just webpack and configure webpack to use esbuild-loader:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: 'esbuild-loader',
options: {
target: 'es2020',
},
},
],
},
};
Enter fullscreen mode Exit fullscreen mode
This configuration tells webpack to use esbuild for transpilation. The build stays identical, but execution time drops by 60-70% because esbuild processes files 10x faster than tsc.
Finally, update your CI configuration to run npm run typecheck as a separate job. The build job no longer calls tsc at all. Both jobs run in parallel, and deployment waits for both to succeed.
# Before
jobs:
build:
steps:
- run: npm run build # calls tsc internally
# After
jobs:
typecheck:
steps:
- run: npm run typecheck
build:
steps:
- run: npm run build # uses esbuild only
Enter fullscreen mode Exit fullscreen mode
One common issue during migration: teams discover that their code has type errors that tsc previously ignored because it was configured loosely. Enable strict: true in tsconfig.json before adding noEmit to catch these issues locally. Do not wait for CI to fail.
Another gotcha is path aliases. If your code uses import { foo } from '@/utils', you need a plugin to resolve these during bundling. esbuild requires esbuild-plugin-path-alias. webpack uses tsconfig-paths-webpack-plugin. swc has built-in support via .swcrc. Configure this before removing tsc or your builds will break with "module not found" errors.
The payoff is immediate: builds finish faster, CI queues drain faster, and developers see type errors within seconds of saving a file. The type-checking feedback loop becomes as fast as linting.
Frequently Asked Questions
Does --noEmit skip type-checking entirely?
No—it performs full type analysis but skips code generation. Every type error still reports, and the compiler exits with a non-zero code if types fail. The only difference is that no .js or .d.ts files appear in your output directory.
Can I use esbuild for both building and type-checking?
No—esbuild does not validate types. It only strips type annotations and produces JavaScript. You must run tsc --noEmit separately to catch type errors. This is intentional: esbuild prioritizes speed over correctness.
What if I need declaration files for an npm package?
Use a separate tsconfig.build.json with "emitDeclarationOnly": true and "noEmit": false. Run tsc -p tsconfig.build.json to generate .d.ts files alongside your bundled output. Application code should never emit declarations.
Does TypeScript 6.0 native execution replace tsc entirely?
Only for local development. You still need tsc --noEmit in CI to validate types. Native execution strips types without checking them, so runtime crashes can slip through. The correct pattern is native execution during development and parallel type-checking in CI.
How do I handle monorepos with project references?
Run tsc --noEmit -b at the root to type-check all packages in dependency order. Each package's build script invokes esbuild or swc independently. TypeScript's composite project feature ensures cross-package types resolve correctly without emitting intermediate files.
That covers the essential patterns for type-only builds in TypeScript 6.0. Apply these in production and your CI pipeline will finish 60-80% faster with identical type safety. The distinction between type-checking and transpilation is no longer optional—modern tooling demands that you separate these concerns or accept slow builds. The failure mode for ignoring this is measurable: every extra minute in CI costs your team deployment velocity and compounds over hundreds of builds per month.







