{"slug": "typescript-6-0-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-tsc", "title": "TypeScript 6.0 `--noEmit` and Type-Only Builds: Why Your CI Pipeline Should Never Call tsc for Output Again", "summary": "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.", "body_md": "`--noEmit`\n\nand Type-Only Builds: Why Your CI Pipeline Should Never Call tsc for Output Again\n\nThis article was written with the assistance of AI, under human supervision and review.\n\nMost 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`\n\nto 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.\n\nThe `--noEmit`\n\nflag 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`\n\nto emit files is a second your deployment sits in a queue.\n\nTypeScript 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`\n\nfiles. This means `tsc`\n\ncan focus exclusively on what it does best: validating types. Production builds never touch the TypeScript compiler's output logic.\n\nThat 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.\n\n`--noEmit`\n\nflag makes tsc a pure type-checker that never writes files, enabling parallel builds with faster transpilers.`tsc --noEmit`\n\nin parallel with your bundler, catching type errors without blocking the build.`.d.ts`\n\n) still require tsc for libraries, but application code should never emit them.The `--noEmit`\n\ncompiler option tells TypeScript to perform its full type analysis without writing any output files. This distinction is critical. When you run `tsc`\n\nwithout 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.\n\n```\n// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"noEmit\": true,\n    \"strict\": true,\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\"\n  }\n}\n```\n\nWith this configuration, running `tsc`\n\nexits 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.\n\nThe 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.\n\nThe configuration also enables TypeScript 6.0's `moduleResolution: \"bundler\"`\n\nmode, 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.\n\nOne caveat applies to library authors: if you publish a package to npm, you need declaration files (`.d.ts`\n\n). These require `tsc`\n\nto emit them because no other tool generates TypeScript's type definitions. For libraries, use a separate `tsconfig.build.json`\n\nwith `\"noEmit\": false`\n\nand `\"emitDeclarationOnly\": true`\n\n. Application code should never touch this configuration.\n\nA production CI pipeline needs two parallel jobs: one runs `tsc --noEmit`\n\nto 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.\n\n```\n# .github/workflows/ci.yml\nname: CI\non: [push, pull_request]\n\njobs:\n  typecheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n      - run: npm ci\n      - run: npm run typecheck\n\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n      - run: npm ci\n      - run: npm run build\n      - uses: actions/upload-artifact@v4\n        with:\n          name: dist\n          path: dist/\n```\n\nThe `typecheck`\n\nscript in `package.json`\n\nruns `tsc --noEmit`\n\n. The `build`\n\nscript 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.\n\n```\n// package.json\n{\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"build\": \"esbuild src/index.ts --bundle --outdir=dist --platform=node\",\n    \"dev\": \"esbuild src/index.ts --bundle --outdir=dist --watch\"\n  }\n}\n```\n\nThis 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.\n\nFor monorepos, run `tsc --noEmit`\n\nonce 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.\n\nOne common mistake is running `tsc`\n\ninside the build script itself. Teams do this because legacy tooling required it, but modern bundlers handle TypeScript natively. Your `build`\n\nscript should invoke webpack, esbuild, or swc directly. The typechecking happens elsewhere.\n\nThree tools dominate TypeScript compilation in 2026, and each serves a distinct purpose. Understanding their tradeoffs determines whether your build is fast, correct, or both.\n\nThe TypeScript compiler (`tsc`\n\n) 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.\n\nesbuild 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`\n\nfor transpilation, but you get zero type safety. If you pass invalid TypeScript to esbuild, it produces broken JavaScript without warning.\n\nswc (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`\n\nseparately.\n\nThe 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`\n\nwait 4-6 minutes for builds that should take 90 seconds.\n\nOne exception applies: if you use Babel with `@babel/preset-typescript`\n\n, 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.\n\nThe correct pattern is `tsc --noEmit`\n\nfor 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.\n\nA 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.\n\nHere is a complete GitHub Actions configuration that implements this pattern with caching and failure handling:\n\n```\n# .github/workflows/deploy.yml\nname: Deploy\non:\n  push:\n    branches: [main]\n\njobs:\n  typecheck:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n          cache: 'npm'\n      - run: npm ci --prefer-offline\n      - run: npm run typecheck\n\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n          cache: 'npm'\n      - run: npm ci --prefer-offline\n      - run: npm run build\n      - uses: actions/upload-artifact@v4\n        with:\n          name: dist\n          path: dist/\n          retention-days: 7\n\n  deploy:\n    needs: [typecheck, build]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/download-artifact@v4\n        with:\n          name: dist\n      - run: |\n          # Deploy dist/ to your hosting provider\n          echo \"Deploying to production\"\n```\n\nThe `needs: [typecheck, build]`\n\ndirective 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.\n\nThe `cache: 'npm'`\n\noption in `setup-node`\n\nreuses `node_modules`\n\nbetween runs. This cuts install time from 60 seconds to 5-10 seconds because npm only fetches changed packages. The `--prefer-offline`\n\nflag tells npm to use cached tarballs when available, which matters for private registries with rate limits.\n\nOne critical detail: the `typecheck`\n\njob does not upload artifacts. It only validates types and exits. The `build`\n\njob uploads the bundled output, which the `deploy`\n\njob downloads. This separation keeps artifacts small and avoids storing intermediate files that CI never uses.\n\nFor 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:\n\n```\n// tsconfig.json (root)\n{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"noEmit\": true\n  },\n  \"references\": [\n    { \"path\": \"./packages/api\" },\n    { \"path\": \"./packages/web\" }\n  ]\n}\n```\n\nRunning `tsc --noEmit`\n\nat 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.\n\nThe failure mode here is running `tsc`\n\nwithout `--noEmit`\n\ninside 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`\n\nfor types and `esbuild`\n\nfor output.\n\nTypeScript 6.0 introduces native execution mode, which allows running `.ts`\n\nfiles directly in Node.js without transpilation. This feature eliminates the need for `ts-node`\n\n, `tsx`\n\n, or any other runtime wrapper during development. The TypeScript compiler embeds itself into the Node.js runtime and strips types on-the-fly.\n\nThis matters because development workflows no longer need a separate build step. Developers run `node --experimental-strip-types src/index.ts`\n\nand 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.\n\nIn 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`\n\n. The compiler never emits files except for libraries that need declaration files.\n\nThe 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`\n\nin watch mode during development.\n\n```\n// package.json\n{\n  \"scripts\": {\n    \"dev\": \"concurrently \\\"tsc --noEmit --watch\\\" \\\"node --watch --experimental-strip-types src/index.ts\\\"\",\n    \"build\": \"esbuild src/index.ts --bundle --outdir=dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n```\n\nThe `concurrently`\n\npackage 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.\n\nOne 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.\n\nThe long-term implication is that tooling complexity decreases dramatically. Teams no longer need `ts-node`\n\n, `tsx`\n\n, `tsconfig-paths`\n\n, or loader hooks. The runtime handles TypeScript natively. This reduces dependency count, eliminates version conflicts, and makes onboarding new developers faster.\n\nMigrating from a legacy build that uses `tsc`\n\nfor 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.\n\nStart by adding `\"noEmit\": true`\n\nto your `tsconfig.json`\n\nand creating a new `typecheck`\n\nscript in `package.json`\n\n. Run this script locally to verify it catches the same errors as your current build. Do not change the build script yet.\n\n```\n// tsconfig.json (before)\n{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"target\": \"ES2020\"\n  }\n}\n\n// tsconfig.json (after)\n{\n  \"compilerOptions\": {\n    \"noEmit\": true,\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\"\n  }\n}\n```\n\nThe `outDir`\n\nand `rootDir`\n\noptions become irrelevant when `noEmit`\n\nis enabled. Remove them to avoid confusion. The `moduleResolution: \"bundler\"`\n\nsetting tells TypeScript to trust your bundler's import resolution instead of enforcing its own rules.\n\nNext, replace `tsc`\n\nin your build script with esbuild or swc. If you currently run `tsc && webpack`\n\n, change it to just `webpack`\n\nand configure webpack to use `esbuild-loader`\n\n:\n\n```\n// webpack.config.js\nmodule.exports = {\n  module: {\n    rules: [\n      {\n        test: /\\.ts$/,\n        loader: 'esbuild-loader',\n        options: {\n          target: 'es2020',\n        },\n      },\n    ],\n  },\n};\n```\n\nThis 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.\n\nFinally, update your CI configuration to run `npm run typecheck`\n\nas a separate job. The build job no longer calls tsc at all. Both jobs run in parallel, and deployment waits for both to succeed.\n\n```\n# Before\njobs:\n  build:\n    steps:\n      - run: npm run build  # calls tsc internally\n\n# After\njobs:\n  typecheck:\n    steps:\n      - run: npm run typecheck\n  build:\n    steps:\n      - run: npm run build  # uses esbuild only\n```\n\nOne common issue during migration: teams discover that their code has type errors that tsc previously ignored because it was configured loosely. Enable `strict: true`\n\nin `tsconfig.json`\n\nbefore adding `noEmit`\n\nto catch these issues locally. Do not wait for CI to fail.\n\nAnother gotcha is path aliases. If your code uses `import { foo } from '@/utils'`\n\n, you need a plugin to resolve these during bundling. esbuild requires `esbuild-plugin-path-alias`\n\n. webpack uses `tsconfig-paths-webpack-plugin`\n\n. swc has built-in support via `.swcrc`\n\n. Configure this before removing tsc or your builds will break with \"module not found\" errors.\n\nThe 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.\n\nNo—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`\n\nor `.d.ts`\n\nfiles appear in your output directory.\n\nNo—esbuild does not validate types. It only strips type annotations and produces JavaScript. You must run `tsc --noEmit`\n\nseparately to catch type errors. This is intentional: esbuild prioritizes speed over correctness.\n\nUse a separate `tsconfig.build.json`\n\nwith `\"emitDeclarationOnly\": true`\n\nand `\"noEmit\": false`\n\n. Run `tsc -p tsconfig.build.json`\n\nto generate `.d.ts`\n\nfiles alongside your bundled output. Application code should never emit declarations.\n\nOnly for local development. You still need `tsc --noEmit`\n\nin 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.\n\nRun `tsc --noEmit -b`\n\nat 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.\n\nThat 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.", "url": "https://wpnews.pro/news/typescript-6-0-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-tsc", "canonical_source": "https://dev.to/jsmanifest/typescript-60-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-call-tsc-for-4jmb", "published_at": "2026-07-22 17:46:59+00:00", "updated_at": "2026-07-22 18:00:51.511568+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["TypeScript", "esbuild", "swc", "webpack", "Rollup"], "alternates": {"html": "https://wpnews.pro/news/typescript-6-0-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-tsc", "markdown": "https://wpnews.pro/news/typescript-6-0-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-tsc.md", "text": "https://wpnews.pro/news/typescript-6-0-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-tsc.txt", "jsonld": "https://wpnews.pro/news/typescript-6-0-noemit-and-type-only-builds-why-your-ci-pipeline-should-never-tsc.jsonld"}}