skip to content

You lead a monorepo where `tsc` declaration emit dominates build time. What does TypeScript's `isolatedDeclarations` option require of the code, what does it buy, and how would you decide whether to adopt it?

level: principalimportance: nice to knowfreq 18%

answer

  1. declarations need whole-program inference
  2. emit becomes the serial bottleneck
  3. the same bargain isolatedModules makes
  4. one file must be enough
  5. explicit annotations on every export

basics

~20 s

isolatedDeclarations, added in TypeScript 5.5, makes the compiler reject exports whose declarations cannot be produced from a single file alone, forcing explicit annotations on the public surface. That guarantee lets other tools emit .d.ts files per file, in parallel, without whole-program inference.

solid answer

~50 s

Normal declaration emit needs the checker: to write `.d.ts` for a module, TypeScript must infer the types of its exports, which can pull in the whole dependency graph. That is why declaration emit is usually the slow, serial part of a monorepo build. `isolatedDeclarations` (TypeScript 5.5 and later) flips the contract: it reports an error wherever an exported declaration lacks enough explicit annotation to produce its `.d.ts` **from that one file**. Once your code satisfies it, declaration emit becomes a per-file syntactic transform, so external tools can do it in parallel — the same bargain `isolatedModules` makes for JavaScript emit. It requires `declaration` (or `composite`) to be on, and it does not by itself make `tsc` faster; the win comes from letting a faster tool take the job, usually paired with `emitDeclarationOnly` in the checking project. The cost is annotation churn across every public export, which is why it lands best in internal packages where the build is the bottleneck.

code

typescript · 9 lines
typescript
import { buildConfig, type Config } from "./config";

// Annotated so the declaration can be emitted from this file alone.
export const settings: Config = buildConfig();

export function makeId(prefix: string): string {
  const suffix = Date.now();
  return `${prefix}-${suffix}`;
}

go deeper

for a junior

Know that declaration files are normally generated by the compiler inferring the types of your exports, and that inference is what makes that step expensive.

for a middle

Explain that the option errors on any export whose declaration cannot be produced from its own file, that the fix is explicit annotations on exported values and return types, and that it requires declaration or composite.

for a senior

Show that the benefit is enabling parallel per-file declaration emit by another tool rather than speeding up tsc, and that it pairs with emitDeclarationOnly in a split check-and-emit pipeline.

for a principal

Own the decision: measure whether declaration emit is really the critical path, weigh annotation churn against project references and incremental builds, scope adoption per package, and settle the team's stance on mandatory public signatures before flipping it.

## Why declaration emit is the slow part Emitting `.js` is close to a per-file transform: strip types, downlevel syntax, write output. Emitting `.d.ts` is not. Consider: ```ts import { buildConfig } from './config'; export const settings = buildConfig(); ``` To write the declaration for `settings`, the compiler must know what `buildConfig` returns, which may require checking `./config` and everything it imports. Declaration emit is therefore a whole-program operation, and in a monorepo it serialises: package B cannot be checked until package A's declarations exist. ## What the option demands `"isolatedDeclarations": true` makes the compiler report an error anywhere an export cannot be declared from its own file. The fix is always the same shape — write the type down: ```ts import { buildConfig, type Config } from './config'; export const settings: Config = buildConfig(); // return type required, not inferred export function makeId(prefix: string): string { return `${prefix}-${Date.now()}`; } ``` Non-exported code is untouched; inference inside function bodies and on local variables works exactly as before. The requirement lands only on the public surface of each module. ## What that guarantee unlocks Once every exported declaration is spelled out, producing a `.d.ts` becomes syntax-directed: read one file, copy the annotations, drop the bodies. Any tool can do it, files can be processed in parallel, and no cross-file type information is needed. This is deliberately the same bargain `isolatedModules` strikes for JavaScript emit — accept a constraint on the source so a single-file transpiler can be correct. It requires `declaration` or `composite` to be enabled, since it constrains declaration output. It pairs naturally with `emitDeclarationOnly` in a setup where `tsc` is the type-check gate and something else produces the runtime JavaScript. Note clearly what it does **not** do: it does not speed up `tsc` itself. Type checking still needs the checker. The gain is architectural — it makes declaration emit a job you can move off the critical path. ## The adoption decision **Measure first.** The prize only exists if declaration emit is genuinely your bottleneck. In many repos the checking pass dominates instead, and this option changes nothing there. Time a clean build with and without `emitDeclarationOnly` before committing. **Count the annotation surface.** The migration cost scales with how many exports rely on inference. A package that already annotates its public API costs almost nothing; one that re-exports inferred values everywhere is a large mechanical change. That change is not pure overhead — explicit public signatures are also a stability win, because a refactor inside a function can no longer silently alter your published type — but it is real work and it touches many files. **Scope it.** This is a per-project setting, so adopt it package by package. Turning it on for a leaf library with a small surface proves the workflow before you commit the whole repo. **Weigh it against the alternatives.** Project references with `composite` and incremental builds already cut rebuild cost by avoiding redundant work; that is often the cheaper first move. `isolatedDeclarations` matters most where you want a non-`tsc` tool to own emit entirely. **Consider the ergonomics.** Some engineers experience mandatory return-type annotations as friction, particularly on small internal helpers that happen to be exported. Others consider it good practice regardless. Land that debate before flipping the flag, not after the first review cycle. ## A reasonable rollout 1. Measure declaration-emit time in isolation and confirm it is the bottleneck. 2. Enable the option in one leaf package; fix the errors it reports — they are mechanical and the compiler tells you exactly where. 3. Wire the alternative emitter for that package's declarations and re-measure end to end. 4. Extend outward only if the numbers justify the churn, and write down the standard so new code arrives compliant. The honest summary: a targeted build-performance tool with a real source-level cost, worth adopting when declaration emit is measurably on the critical path and not before.

  • Does turning on `isolatedDeclarations` make `tsc` itself faster?
    No, and claiming so is the usual misunderstanding. Type checking still requires the full checker. What changes is that declaration emit becomes a per-file syntactic transform, so a different tool can produce the `.d.ts` files in parallel and off the critical path. The speed-up comes from that reorganisation, not from the flag.
  • How is this related to `isolatedModules`?
    They are the same trade in two places. `isolatedModules` constrains source so a single-file transpiler can emit correct JavaScript without cross-file type information; `isolatedDeclarations` constrains exports so a tool can emit correct declarations the same way. Both accept an authoring restriction in exchange for parallelisable, tool-agnostic emit.
  • What is the non-performance argument for annotating every exported declaration?
    Stability of the published surface. When a public export's type is inferred, an unrelated edit inside a function body can silently change what consumers see. Writing the signature down makes the public contract explicit and turns such a change into a local error at the definition instead of a surprise in a downstream build.
  • Where would you start a rollout in a large monorepo?
    In a single leaf package with a small public surface, after measuring that declaration emit — not checking — is actually the bottleneck. It is a per-project setting, so a leaf proves the toolchain end to end at low cost. Extend only if the measured gain justifies the annotation churn, and document the standard so new code complies by default.

saying these in an interview costs you the question

  • Claims the option makes tsc's type checking faster
  • Thinks it requires annotating local variables and internals too
  • Believes it replaces the declaration option rather than requiring it
  • Adopts it repo-wide without measuring where build time goes
  • Confuses it with isolatedModules, which governs JavaScript emit

context