cd /news/developer-tools/typescript-6-0-noemit-and-type-only-… · home topics developer-tools article
[ARTICLE · art-69007] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

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

A developer argues that TypeScript's `--noEmit` flag should be used in CI pipelines to separate type-checking from code generation, enabling parallel builds with faster transpilers like esbuild or swc. This approach can reduce build times by 60-80% while maintaining type safety, though library authors still need `tsc` for declaration files.

read13 min views1 publishedJul 22, 2026

--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.

--noEmit

flag makes tsc a pure type-checker that never writes files, enabling parallel builds with faster transpilers.tsc --noEmit

in parallel with your bundler, catching type errors without blocking the build..d.ts

) still require tsc for libraries, but application code should never emit them.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"
  }
}

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.

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.

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/

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"
  }
}

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.

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.

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:

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: |
          echo "Deploying to production"

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" }
  ]
}

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 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"
  }
}

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 hooks. The runtime handles TypeScript natively. This reduces dependency count, eliminates version conflicts, and makes onboarding new developers faster.

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"
  }
}

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-

:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        : 'esbuild-',
        options: {
          target: 'es2020',
        },
      },
    ],
  },
};

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.

jobs:
  build:
    steps:
      - run: npm run build  # calls tsc internally

jobs:
  typecheck:
    steps:
      - run: npm run typecheck
  build:
    steps:
      - run: npm run build  # uses esbuild only

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.

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.

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.

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.

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.

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @typescript 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/typescript-6-0-noemi…] indexed:0 read:13min 2026-07-22 ·