CodeFixesHub
    programming tutorial

    Using noEmitOnError and noEmit in TypeScript: When & How to Control Emitted Output

    Learn when to use noEmitOnError vs noEmit in TypeScript. Practical examples, debugging steps, and CI recommendations — take control of your builds now.

    article details

    Quick Overview

    TypeScript
    Category
    Aug 21
    Published
    22
    Min Read
    2K
    Words
    article summary

    Learn when to use noEmitOnError vs noEmit in TypeScript. Practical examples, debugging steps, and CI recommendations — take control of your builds now.

    Using noEmitOnError and noEmit in TypeScript: When & How to Control Emitted Output

    Introduction

    TypeScript's compiler (tsc) gives you fine-grained control over whether compiled JavaScript (and declaration files) are produced. Two flags you will encounter often are noEmitOnError and noEmit. For intermediate developers who manage larger codebases, monorepos, or CI pipelines, knowing when to block output and when to simply suppress it can save debugging time, prevent shipping broken artifacts, and improve developer experience.

    In this tutorial you'll learn:

    • The exact behavioral difference between noEmitOnError and noEmit (and related flags).
    • How these flags interact with tsconfig, CLI, tsc --watch, and project references (--build).
    • Practical examples showing how to configure tsconfig for local dev, CI, and library builds.
    • How to troubleshoot cases when emit still happens unexpectedly and how to integrate with bundlers (Babel, ts-loader).
    • Performance and advanced techniques to optimize compile-and-emit workflows.

    We'll include runnable configuration examples and step-by-step scenarios you can copy into your projects. You'll also find links to related TypeScript topics (decorators, complex type behavior, project structure) that frequently intersect with emit behavior. By the end you'll be able to choose the appropriate strategy for local development, continuous integration, and library publishing.

    Background & Context

    TypeScript is both a type-checker and a transpiler. In some workflows you want only type checking (no output), while in others you want compiled JS as the final artifact. Historically, TypeScript offered two flags to control emission:

    • noEmitOnError: Prevent emitting output files if any diagnostics are reported (type errors or syntactic errors). If false, TypeScript will still emit even when errors exist.
    • noEmit: Disable emitting output entirely — TypeScript will only type-check and not write any files.

    Understanding these flags matters for developer feedback loops and CI. For example, you might want to block CI artifacts if the codebase has errors (use noEmitOnError) but want to run a local type-checker without producing artifacts (use noEmit). They also interact with other options like incremental builds, declaration emission (declaration, emitDeclarationOnly), decorators metadata, and project references.

    Key Takeaways

    • noEmitOnError stops the compiler from producing files when there are errors; noEmit prevents emitting files altogether.
    • Use noEmit for type-checking only workflows (e.g., pre-commit hooks or editor checks). Use noEmitOnError for build pipelines where you want to prevent broken artifacts.
    • watch and project-build behaviors differ — verify combined flags when using --watch or --build.
    • Emission-related surprises often stem from using third-party toolchains (Babel, ts-loader) or multi-project builds.
    • Complex type computations can slow compile or produce many diagnostics; reducing type complexity or using incremental builds helps.

    Prerequisites & Setup

    Before proceeding, ensure you have the following:

    • Node.js (12+) and npm/yarn installed.
    • TypeScript installed in your project (npm i -D typescript) or globally for quick experiments (npm i -g typescript).
    • A basic tsconfig.json for the project root. We'll show sample configurations below.
    • Optional: a bundler (webpack) or Babel if you plan to test integration; a monorepo with project references for build scenarios.

    Example to install TypeScript locally:

    bash
    npm init -y
    npm install --save-dev typescript
    npx tsc --init

    Now you're ready to experiment with flags and configurations.

    Main Tutorial Sections

    1) noEmitOnError vs noEmit — Clear definitions and quick examples

    noEmitOnError: boolean. When true, tsc will not emit any output files if there are any semantic or syntactic errors. Example in tsconfig:

    json
    {
      "compilerOptions": {
        "noEmitOnError": true,
        "outDir": "dist"
      }
    }

    noEmit: boolean. When true, tsc skips emitting files regardless of errors. Useful for running type checks only (e.g., in pre-commit hooks or CI validations):

    json
    {
      "compilerOptions": {
        "noEmit": true
      }
    }

    CLI equivalents: npx tsc --noEmitOnError and npx tsc --noEmit.

    2) Using the flags in different workflows (local dev, CI, libraries)

    Local development: many teams want fast feedback and don't want a full emit on every save. Use --noEmit for ad-hoc type-checks (or let your editor run the language service). Example script in package.json:

    json
    {"scripts": {"typecheck": "tsc --noEmit"}}

    CI builds: production artifacts should not be emitted if there are type errors — use --noEmitOnError or set it in tsconfig and run tsc normally. For packages, you might combine emitDeclarationOnly with noEmitOnError to ensure declarations are correct before publishing.

    Library publishing: you may want to emit .d.ts files even when JS emission is controlled. Use emitDeclarationOnly: true to produce only declarations while honoring noEmitOnError.

    3) Interaction with --watch and incremental build modes

    --watch continuously recompiles; noEmitOnError still prevents writing files if errors are present on a given compilation cycle. However, some watchers or bundlers that integrate with TypeScript might bypass tsc's emit and perform their own transpilation (e.g., Babel or ts-loader with transpileOnly). Be aware that:

    • Using --incremental writes .tsbuildinfo files to speed up future builds.
    • --noEmit prevents .js and .d.ts but will still produce .tsbuildinfo if incremental is enabled. If you want to avoid build info creation, avoid incremental or set --incremental false.

    Example:

    bash
    npx tsc --watch --noEmitOnError

    This will watch and not output files when errors exist.

    4) Project references and tsc --build behaviour

    When using project references and tsc --build (or tsc -b), the build mode coordinates across referenced projects. By default, tsc -b will fail a build if a referenced project fails, and will not emit outputs for the failed project. However, subtle behaviors can arise:

    • If you set composite: true in referenced projects, TypeScript writes .tsbuildinfo and may try to reuse outputs.
    • noEmitOnError on the root config is honored, but you should ensure every referenced project is configured properly.

    Example root tsconfig for build:

    json
    {
      "files": [],
      "references": [{"path": "./packages/pkg-a"}, {"path": "./packages/pkg-b"}]
    }

    Then npx tsc -b will run referenced builds and stop emits for failed projects.

    5) When emit still happens — common pitfalls and how to debug

    If you set noEmitOnError: true but still see .js files, check these common causes:

    • A bundler like Babel or esbuild is transpiling TypeScript source directly and producing output independently of tsc.
    • A loader like ts-loader in transpileOnly mode bypasses type checking and always emits JS. Use transpileOnly: false or run a separate tsc check step.
    • Running tsc with a different config file or CLI overrides — verify the config used with npx tsc -p tsconfig.json.
    • Editor or language server emitting via some plugin.

    Troubleshooting steps:

    1. Run npx tsc --noEmitOnError --pretty in the project root to see diagnostics.
    2. Check package.json scripts and bundler configuration for other transpilers.
    3. Add an explicit script typecheck and enforce it in CI to ensure tsc is used.

    6) Emit plus decorator metadata and why it matters

    If you use decorators and rely on runtime metadata, TypeScript's emit settings matter. To produce decorator metadata you enable experimentalDecorators and emitDecoratorMetadata in tsconfig. Emitting files is required so that the emitted JavaScript contains necessary decorator calls and metadata — but you may still want to block emit on errors.

    Example relevant options:

    json
    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
      }
    }

    If you're working with decorators, check our overview of Decorators in TypeScript: Usage and Common Patterns for advice on metadata and runtime behavior. Deeper decorator patterns (property, method, parameter) are covered in dedicated guides: Property decorators explained, Method decorators explained, and Parameter decorators explained.

    7) Complex types, heavy inference, and their impact on emit

    Large or deeply recursive types (conditional types, mapped types, infer usage) can increase compile time and generate many diagnostics. This indirectly affects decisions on emitting: if your type system produces hundreds of errors, noEmitOnError will block emit until they are fixed.

    If you maintain utilities that heavily use infer, conditional types, or recursive mapped types, these articles are good references for optimizing and understanding performance: Using infer with functions in conditional types, Recursive conditional types for complex type manipulations, and Recursive mapped types for deep transformations in TypeScript.

    Practical mitigation strategies:

    • Split extremely complex types into smaller named types.
    • Use any in narrow internal boundaries during refactor and reintroduce stricter types incrementally.
    • Use skipLibCheck to avoid spending time on library types while debugging your code.

    8) Integration scenarios: Babel, ts-loader, and transpile-only setups

    Modern toolchains sometimes separate type-checking from transpilation. For example, Babel often handles emit (via @babel/preset-typescript) and intentionally skips type checking. In these scenarios:

    • Use noEmit or run tsc --noEmit in CI to ensure types are enforced even if Babel handles transforms.
    • With webpack + ts-loader, avoid transpileOnly: true unless you have a separate type-check step (fork-ts-checker-webpack-plugin).

    Example package.json scripts pattern:

    json
    {
      "scripts": {
        "build": "webpack",
        "typecheck": "tsc --noEmit",
        "ci": "npm run typecheck && npm run build"
      }
    }

    This guarantees that build only runs after type checking has succeeded.

    9) Declaration files and emitDeclarationOnly interplay

    If you publish libraries, .d.ts files are crucial. TypeScript has declaration and emitDeclarationOnly. Use emitDeclarationOnly: true if you only need declaration files (for instance when a bundler produces JS). Pair it with noEmitOnError to block publishing if declarations have errors.

    Example tsconfig for declarations:

    json
    {
      "compilerOptions": {
        "declaration": true,
        "emitDeclarationOnly": true,
        "outDir": "types",
        "noEmitOnError": true
      }
    }

    When emitting only declarations, remember that noEmit will also prevent .d.ts files. If your goal is to only type-check and not emit anything, use noEmit. If you want declarations but not JS, use emitDeclarationOnly.

    10) File-based examples: try it locally

    Create a small project to see the flags behave:

    1. Initialize:
    bash
    mkdir ts-emit-demo && cd ts-emit-demo
    npm init -y
    npm i -D typescript
    npx tsc --init
    1. Edit tsconfig.json to have:
    json
    {
      "compilerOptions": {
        "target": "ES2017",
        "module": "commonjs",
        "outDir": "dist",
        "noEmitOnError": true
      }
    }
    1. Create a file src/index.ts with an intentional error:
    ts
    export function greet(name: string) {
      return `Hello, ${name.toUpperCase()}`;
    }
    
    // Intentional error: missing parameter type
    const x = greet(123);
    1. Run npx tsc -p . — you should see diagnostics and no dist/ files created.

    2. Toggle noEmitOnError to false and rerun — you will see dist/index.js emitted even though errors exist (not recommended for CI builds).

    Advanced Techniques

    • Use tsc --build with project references and composite: true to split large repos into smaller compile units; this often reduces the blast radius of errors and improves incremental build times.
    • Combine noEmitOnError with --incremental and --tsBuildInfoFile to speed builds while ensuring you don't ship artifacts with errors. Remember .tsbuildinfo files are written even when no JS is emitted if incremental is enabled.
    • For extremely large type computations, use @ts-expect-error and @ts-ignore sparingly to localize temporary workarounds, and add tests to avoid regressions.
    • When using Babel-based pipelines, run tsc --noEmit in CI to enforce typings separately from transforms.
    • Use emitDeclarationOnly to generate types for downstream consumers while leaving the JS pipeline to bundlers.

    Also consider how advanced TypeScript features affect compile performance — our deep-dives on Using infer with objects, Distributional conditional types and Advanced mapped types: Key remapping offer patterns to simplify or rework heavy type logic.

    Best Practices & Common Pitfalls

    Dos:

    • Do use noEmitOnError in CI and publish pipelines to avoid releasing broken artifacts.
    • Do create a typecheck script (tsc --noEmit) and run it as part of tests or pre-commit checks.
    • Do keep tsconfigs small and scoped: large globs cause unnecessary compile overhead.
    • Do prefer emitDeclarationOnly for libraries that use separate bundlers for JS output.

    Don'ts:

    • Don’t rely on Babel or loaders that skip type checking as your only type enforcement mechanism — run tsc --noEmit in CI.
    • Don’t overuse @ts-ignore to silence errors; prefer narrow, documented exceptions.
    • Don’t assume project references automatically respect all emission flags — test your build pipeline.

    Troubleshooting tips:

    • If you see emitted files when you expected none, search your build pipeline for other transpilers (Babel, esbuild, ts-loader with transpileOnly true).
    • If compilation is slow, profile type-checking by isolating heavy files and reviewing recursive type usage. Our article on Recursive conditional types can help identify problematic patterns.

    Real-World Applications

    • Monorepos: Use tsc --build with noEmitOnError for CI to ensure no package emits incomplete artifacts. Project references reduce unnecessary recompilation across packages.
    • Library publishing: Use emitDeclarationOnly together with noEmitOnError to produce clean type packages for consumers while leaving JS bundling to your build tool.
    • Front-end bundling: If you rely on Babel/webpack to emit JS, run tsc --noEmit as a separate validation step in CI to catch typing regressions early.
    • Decorator-heavy frameworks: Ensure emitDecoratorMetadata is enabled and that your build pipeline emits the JS only when type checks pass. See our guide on Implementing Mixins in TypeScript for patterns that commonly use decorators together with runtime metadata.

    Conclusion & Next Steps

    Choosing between noEmitOnError and noEmit is about policy and workflow. Use noEmit for pure type-checking tasks and noEmitOnError to protect build artifacts. Integrate type-checking into CI and keep your build pipeline explicit about who emits JS. Next, explore performance tuning (incremental builds, project references) and inspect complex type patterns that can slow compilation.

    If you use decorators, declarations, or advanced mapped types, check the related articles linked in this post to deepen your understanding and optimize your configuration.

    Enhanced FAQ

    Q1: What exactly triggers noEmitOnError to block emission — type errors or syntax errors?

    A1: noEmitOnError prevents emitting when there are any diagnostics reported by the compiler, including both syntactic (parsing) and semantic (type) errors. The compiler runs type-checking and diagnostics before attempting to write emitted files; if it finds any errors with severity ‘error’, it will not write output when noEmitOnError is true.

    Q2: If I run tsc --noEmit, will it still create .tsbuildinfo files when using --incremental?

    A2: Yes. If you enable --incremental, TypeScript writes .tsbuildinfo to store incremental build state even when --noEmit is used. If you want to avoid creating build info files, do not enable incremental for a purely no-emit type-check step.

    Q3: Why is my bundler still producing JS despite setting noEmitOnError in tsconfig?

    A3: Many bundlers or transpilers handle TypeScript by themselves (Babel, swc, esbuild) or through loaders (ts-loader with transpileOnly). These tools may not consult your tsconfig for emit flags or may use a separate transform pipeline that bypasses tsc's emit behavior. Ensure you run tsc --noEmit separately in CI or disable transpile-only modes if you want tsc to gate emission.

    Q4: Should I set noEmitOnError to true locally during development?

    A4: It depends. Some developers prefer fast iteration and let bundlers always emit while relying on tsc --noEmit as a separate check; others want strict local blocking to avoid running code with type errors. A common compromise: use noEmitOnError in CI and noEmit for developer type-checking tasks.

    Q5: How do noEmitOnError and emitDeclarationOnly interact?

    A5: If emitDeclarationOnly is true, TypeScript will only emit declaration files (.d.ts). noEmitOnError still applies — if errors exist, declarations will not be emitted. If you set noEmit to true, no declarations or JS will be emitted at all.

    Q6: Can I rely on TypeScript's noEmitOnError to prevent publishing bad packages in a monorepo?

    A6: It helps, but you should enforce a build-and-publish pipeline that runs tsc -b --noEmitOnError or runs tsc with noEmitOnError configured. For monorepos with project references, ensure each referenced project has composite: true as needed and test tsc -b locally to validate behavior.

    Q7: Does the language service (editor) respect noEmit/noEmitOnError?

    A7: The language service used by editors does type-checking and provides errors but does not necessarily follow tsconfig emit flags — it's focused on interactive experience. Emission is typically controlled by CLI or build tools. Use tsc --noEmit to run a canonical type-check step outside the editor.

    Q8: What are practical steps if a complex generic type causes hundreds of errors that block emit?

    A8: Break down the type into smaller units, add intermediate type aliases to simplify inference, or use // @ts-expect-error in small, controlled places while refactoring. Consider adding unit tests that assert behavior rather than relying solely on type inference. See guidance on complex types and optimizations in our articles on Using infer with arrays, Distributional conditional types and Advanced mapped types.

    Q9: How do decorators and emit flags play together if I rely on runtime metadata?

    A9: Decorators need emitted JavaScript with decorator calls and, if emitDecoratorMetadata is enabled, runtime type metadata. If you run tsc --noEmit for type-checking only, your runtime artifacts aren’t produced. For a runtime test or local debugging that requires metadata, you must use a build step that emits JavaScript (and ensure noEmitOnError won’t block it unexpectedly). For a deeper discussion on decorators patterns and metadata, review Decorators in TypeScript: Usage and Common Patterns and focused decorator guides (Property decorators explained, Method decorators explained, Parameter decorators explained).

    Q10: Any recommended pipeline script patterns to ensure safe builds?

    A10: Yes — a common safe pattern in package.json is:

    json
    {
      "scripts": {
        "typecheck": "tsc --noEmit",
        "build": "webpack",
        "ci": "npm run typecheck && npm run build"
      }
    }

    This enforces type checks before the build. For monorepos, use tsc -b with noEmitOnError enabled for referenced projects.

    If your codebase uses advanced type features (mixins, constructor parameter handling, or instance type extraction), also consult articles like Implementing Mixins in TypeScript, Utility Type: ThisParameterType, and Utility Type: OmitThisParameter for guidance on patterns that commonly surface type-check complexity around emits.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:20:06 PM
    Next sync: 60s
    Loading CodeFixesHub...