loke.dev
Header image for Isolated Declarations Are the Only Way to Scale TypeScript

Isolated Declarations Are the Only Way to Scale TypeScript

Stop waiting for sequential type-checking and unlock true parallel builds in your codebase by adopting the new isolatedDeclarations requirement.

· 5 min read

Isolated Declarations Are the Only Way to Scale TypeScript

We’ve been told for years that TypeScript’s powerful type inference is its greatest feature—that "clean code" means letting the compiler figure out your return types so you don't have to litter your files with boilerplate. This is a lie. Well, maybe not a lie, but a luxury that only works for small projects. If you’re working in a massive monorepo and wondering why your CI takes twenty minutes to run a build, your reliance on "clever" inference is likely the culprit.

The Sequential Wall

Most developers assume that if they have a 64-core build server, TypeScript will just use all those cores to check their code. It doesn't. Because of how TypeScript works, generating declaration files (.d.ts) usually requires a full type-check of the entire project graph.

If Project B depends on Project A, the compiler has to fully finish analyzing Project A to figure out what it exports before it can even start on Project B. This is because if you didn't explicitly write down what a function returns, TypeScript has to go look at the implementation, trace the logic, and "calculate" the shape of that data.

This creates a massive sequential bottleneck. Your expensive CI runner is sitting there, 63 cores idling, while one core struggles to figure out the return type of a deeply nested map function in your core utility library.

What are Isolated Declarations?

Introduced in TypeScript 5.5, the isolatedDeclarations flag forces a shift in how we write code. When enabled, TypeScript won't let you export anything unless its type can be determined "locally"—without looking at other files or running complex inference.

Essentially, you're trading a little bit of manual labor (writing types) for massive, parallelized speed.

The "Lazy" Way (Current Practice)

Here is what most of us write. It looks fine, right?

// utils.ts
export function transformData(input: string) {
  return {
    id: Date.now(),
    data: input.toUpperCase(),
    timestamp: new Date()
  };
}

In a standard setup, if another file imports transformData, TypeScript has to parse utils.ts and infer that return object. If utils.ts is part of a package being built by a fast tool like esbuild or swc, those tools simply cannot generate the .d.ts file because they don't do full type-checking. They see this code and throw their hands up.

The "Scalable" Way (Isolated Declarations)

With isolatedDeclarations: true in your tsconfig.json, the code above will throw an error. You are now required to be explicit:

// utils.ts
interface ProcessedData {
  id: number;
  data: string;
  timestamp: Date;
}

export function transformData(input: string): ProcessedData {
  return {
    id: Date.now(),
    data: input.toUpperCase(),
    timestamp: new Date()
  };
}

It’s more code. It’s "boring." But now, a tool like oxc or swc can look at just this file and immediately know what the declaration file should look like without needing to understand the rest of your dependency graph.

Why This Actually Matters

When you adopt this constraint, you unlock transpiler-based declaration generation.

Instead of tsc being the bottleneck, you can use lightning-fast Rust-based tools to generate your .d.ts files in parallel. We are talking about moving from minutes to seconds.

In a large monorepo with 500 packages, the difference is life-changing. Instead of a "waterfall" build where everything waits for the core library to finish, you can blast through the entire graph simultaneously.

It's Not Just Functions

The requirement extends to classes and complex types too. Consider this common pattern:

// Forbidden under isolatedDeclarations
export class User {
  settings = {
    theme: 'dark' as const,
    notifications: true
  };
}

The compiler will complain because settings is being inferred. To fix it, you have to define the type explicitly:

type UserSettings = {
  theme: 'dark' | 'light';
  notifications: boolean;
};

export class User {
  settings: UserSettings = {
    theme: 'dark',
    notifications: true
  };
}

The "Gotchas" and Edge Cases

Is it annoying? Sometimes. You’ll find yourself hitting cases where you’re exporting a variable that comes from a complex third-party library:

import { someComplexHelper } from 'big-library';

// Error: The type of 'result' must be explicitly declared
export const result = someComplexHelper();

If someComplexHelper returns a massive, 40-property interface, you're going to have to import that type and type-hint it. Yes, it’s more typing. But here’s the reality: if you can’t easily see what the type is by looking at the file, neither can your fellow developers (or the build tool).

By forcing these declarations, you are essentially documenting your public API boundaries.

How to Get Started

You don't have to flip the switch and break your whole repo today.

1. Update to TS 5.5+: This is a requirement.
2. Enable the flag: Add "isolatedDeclarations": true to your compilerOptions.
3. Use the "Fix All" action: Modern IDEs (VS Code) now have a "Fix all isolated declaration errors" action that will automatically add the inferred types to your exports. It's not perfect, but it gets you 90% of the way there.

Final Thoughts

I used to be a "minimalist" who hated explicit return types. I thought they were redundant. But as I've worked on larger and larger codebases, I've realized that inference is for implementation, not for boundaries.

By adopting isolatedDeclarations, you are doing your future self a favor. You're making your codebase machine-readable in a way that allows for extreme parallelization. Stop letting your build process crawl. Write the types, unlock the speed, and go get a coffee because your CI finished before you could even get up from your desk.