Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.aspect.build/llms.txt

Use this file to discover all available pages before exploring further.

TypeScript’s isolatedDeclarations compiler option enables declaration file (.d.ts) generation as a simple single-file transform, with no type checker required. In Bazel, this breaks the sequential chain of type-check actions and unlocks near-infinite parallelism for large TypeScript monorepos.

The problem: type checking on the critical path

TypeScript type checking is single-threaded as a fundamental design constraint. The typical way to mitigate this in Bazel is to split your codebase into many small targets so Bazel can run multiple tsc processes in parallel. However, there’s a more subtle de-optimization. During type checking, TypeScript only needs .d.ts files from dependencies — not their full implementations. However, generating a .d.ts file normally requires running a full type check first. This creates a dependency chain: to type-check target A, you need .d.ts files from B and C; to generate those, you must first type-check B and C; and so on. In Bazel’s action graph, this means type-check actions produce outputs that block other type-check actions, creating a long, unavoidable critical path.
type-check(app)
  └── needs .d.ts from type-check(counter)
        └── needs .d.ts from type-check(textutils)
              └── ...
For a chain like app ← counter ← textutils, sequential compilation might take 5s + 3s + 6s = 16s. With isolated declarations, the same build can complete in roughly 500ms + 6s = 6.5s by running the slow parts in parallel.

The solution: isolated_typecheck with TS isolatedDeclarations option

TypeScript 5.5 introduced the isolatedDeclarations compiler option. When enabled, TypeScript enforces that every exported symbol has an explicit type annotation. Inferred return types are disallowed, as is inferred types from cross-file expressions.
// ❌ Not allowed with isolatedDeclarations — return type is inferred from a dependency
export function countParts(x: string) {
  return new Splitter(x).splitWords().size();
}

// ✅ Explicit annotation makes the signature self-contained
export function countParts(x: string): number {
  return new Splitter(x).splitWords().size();
}
This constraint requires changes to your code, but it means a file’s .d.ts can be derived from its source alone, with no knowledge of its dependencies. Declaration file generation becomes a single-file AST transform.

Fast declaration emitters

Because generating .d.ts no longer requires a type checker, third-party tools can implement the transform with much less overhead. Benchmark for a package of ~20 TypeScript files:
MethodTime
tsc (traditional type check + emit)~860ms
tsc with isolatedDeclarations~340ms
oxc isolated declarations transform~5ms
The oxc transform is around 168× faster than the traditional approach.
isolatedDeclarations was proposed by Martin Probst (Google) in TypeScript issue #47947 and refined by Rob Palmer (Bloomberg), with contributions from companies including Airtable, Figma, Bloomberg, Google, and Shopify. It shipped in TypeScript 5.5.

How it changes the Bazel action graph

With isolated declarations, you introduce a fast declaration emit action that runs with no transitive dependencies—only the source files for that package. The type-check action then consumes these pre-generated .d.ts files instead of waiting for upstream type checks to complete. Before:
type-check(app) ──depends on──▶ type-check(counter) ──depends on──▶ type-check(textutils)
  (produces .d.ts)                (produces .d.ts)                    (produces .d.ts)
After:
declare(app)    declare(counter)    declare(textutils)   ← fast, source-only, fully parallel
     │                │                    │
     ▼                ▼                    ▼
type-check(app)  type-check(counter)  type-check(textutils)  ← no build outputs, fully parallel
Key properties of this new graph:
  • Declaration actions have no transitive dependencies. They run immediately and cache well.
  • Type-check actions have no build outputs. They are pure validation and cannot block anything else.
  • The critical path is at most two actions long, regardless of how deep your dependency graph is.
  • Both stages are fully parallelizable across all available workers (local or RBE).

Pruning the dependency graph

With isolated declarations, you can also emit both the implementation dependency graph and the module-signature dependency graph from your build file generator. Type-check actions can then collect their transitive inputs using the module-signature dependency graph rather than the implementation dependency graph. Since declaration files strip implementation details, they typically have far fewer imports than the full source—so type checks see far fewer files. This eliminates actions that were previously scheduled but provably unnecessary, reducing:
  • Inputs per action
  • Cache validation overhead
  • Total actions scheduled per build

Migration

1

Get your TypeScript codebase green

Add "isolatedDeclarations": true to your root tsconfig.json and fix the errors TypeScript reports.Anywhere a skipped annotation cannot be trivially inferred (exported functions without return types, exported variables initialized from dynamic expressions) TypeScript will require an explicit annotation.Your editor’s quick-fix support can insert most of these automatically. In VS Code, look for the “Infer function return type” and similar code actions on each error, or use a codemod tool like ts-morph to automate the changes across large codebases.Do not move on until tsc --noEmit passes cleanly.
2

Tell Bazel about it

Once your codebase is green, enable isolated_typecheck in your ts_project targets:
ts_project(
    name = "my_lib",
    srcs = glob(["*.ts"]),
    isolated_typecheck = True,
    # ...
)
rules_ts picks up isolatedDeclarations from your tsconfig.json automatically. With isolated_typecheck = True, Bazel splits declaration emit and type checking into separate actions, unlocking the parallelism described above.API reference: ts_project
3

(Optional) Migrate incrementally, starting from the slowest libraries

You don’t have to migrate everything at once. If your codebase is large, you can enable isolatedDeclarations per tsconfig.json and isolated_typecheck per target, starting with the libraries that hurt your build the most.To find the best candidates, look for targets that:
  • Have many dependents (high fan-out in the build graph)
  • Are slow to type-check
  • Sit near the top of your critical path
Run bazel query 'rdeps(..., //my/slow:lib)' to see how many targets depend on a given library. Targets with the most reverse dependencies benefit the most from being unblocked early.
Canva migrated ~90% of their 40,000 packages before seeing the results above. Even partial adoption yields measurable improvements on the targets you do migrate.

Real-world results: Canva

Canva presented their adoption of isolated declarations at BazelCon 2025. Their codebase has ~105,000 TypeScript files across ~40,000 Bazel packages, with one BUILD file and one tsconfig.json per folder. After migrating (approximately 90% complete at time of presentation):
MetricImprovement
Type-check actions per PR−73–81%
Type-check wall time (P95)−53–60% (10 minutes faster)
Type-check wall time (P50)Under 1 minute
Local type-check usage+70% (developers relying less on CI)
In the P95 case, that’s over 10,000 fewer actions scheduled per PR validation run. Canva uses oxc for declaration emit and custom build-file generation tooling to emit both implementation and module-signature dependency graphs into their BUILD files.

Further reading

Community resources that informed this page: