TypeScript 项目不断增长。文件越来越多。依赖关系变得错综复杂。最终,你的构建会遇到瓶颈,这与逻辑的复杂性无关,而完全是因为编译器在编写单个声明文件之前需要读取整个“宇宙”。

TypeScript 6.0 通过 isolatedDeclarations 解决了这个问题。该功能重新思考了 .d.ts 文件的生成方式。它不再将声明发射(declaration emission)与完整的类型检查流水线绑定,而是让编译器通过孤立地查看每个源文件来生成这些文件。其结果是,构建过程可以在数千个文件上并行运行,而不是逐个链接地爬过你的依赖图。

真正的瓶颈

目前,生成声明文件是一个串行操作。当你启用 --declaration 并运行编译器时,在 TypeScript 完全理解该模块涉及的所有类型之前,它无法为给定模块生成 .d.ts 文件。如果 utils.tstypes.ts 导入类型,而 types.ts 又从 api.ts 引入内容,编译器必须在描述 utils.ts 导出的内容之前解析该链条。

在大型 monorepo 中,这种级联效应是极其残酷的。导入图根部附近的一个单一文件可能会阻塞数百个下游文件的声明发射。你的 CPU 有八个核心,但其中七个在 TypeScript 费力地跨包边界重建每个接口的形状时处于闲置状态。编译器在做必要的工作,但类型检查与声明发射之间的耦合意味着,即使你只想将公共表面类型写入磁盘,也必须支付跨文件分析的全额成本。

isolatedDeclarations 如何改变规则

isolatedDeclarations 打破了这种耦合。启用该标志后,编译器同意在不询问其他文件任何含义的情况下,为源文件生成 .d.ts 文件。它通过要求一个简单的契约来实现这一点:每个导出的符号必须在声明处携带显式的、可见的类型注解。

如果编译器能在源文件中直接看到完整的类型,它就不需要进行类型推断(inference)。它不需要追踪导入,也不需要知道另一个文件中的标识符 User 是接口、类型别名还是类。它只需准确地发射你所写的内容。

这意味着文件 A 和文件 B 可以同时生成它们的声明。构建编排器可以将每个文件交给单独的线程。以前因为缺乏完整类型检查器而跳过 .d.ts 生成的快速转译器(transpilers)现在也可以生成声明文件了,因为这项工作变成了纯粹的语法操作。

折衷方案:写下来

速度并非免费。你必须停止在任何导出的内容上依赖类型推断。每个公共函数、类、变量和常量都需要明确写出其类型。如果 TypeScript 必须通过查看返回语句或解析泛型参数来计算类型,isolatedDeclarations 将会报错。

以下是实际应用中的样子。在不使用该标志时,你可能会这样写:

export function fetchUser(id: number) {
  return fetch(`/users/${id}`).then(r => r.json());
}

TypeScript 通过检查 fetch,然后是 Promise.prototype.then,最后是返回 r.json() 的匿名函数来推断返回类型。为了发射 .d.ts,编译器需要执行所有这些分析。

启用 isolatedDeclarations 后,你必须为导出进行注解:

interface User {
  id: number;
  email: string;
}

export function fetchUser(id: number): Promise<User> {
  return fetch(`/users/${id}`).then(r => r.json());
}

现在编译器能立即看到 Promise<User>。它发射声明并继续下一步。

这条规则适用范围很广。导出的数组需要显式类型,而不是从其元素中推断。如果形状对消费者很重要,导出的对象需要显式的类型注解。泛型函数需要在声明处显示其返回类型和约束。你不能在不给复杂的映射类型(mapped type)提供完整的命名类型别名的情况下导出其结果。

好处是你的公共 API 变得具有自文档化特性。消费者(以及编译器)不再需要从实现细节中反向工程你的意图。类型本身就是一个明确的契约。

时间花在了哪里

在大型代码库中,影响是立竿见影的。构建时间从几分钟缩短到几秒钟,因为声明发射不再是整个流程中最耗时的环节。每个文件独立发射,因此该过程随你的核心数扩展,而不是随导入图的深度扩展。

This also changes what tools you can use. Transpilers like esbuild and swc are already lightning-fast at turning TypeScript into JavaScript, but many teams still run tsc separately just to produce .d.ts files. With isolatedDeclarations, those fast tools can handle both jobs. They do not need to replicate TypeScript's entire type system to generate declarations; they only need to parse syntax and copy the explicit types you provided. That makes end-to-end TypeScript builds with alternative toolchains far more viable.

Distributed and incremental builds get simpler too. In continuous integration, a remote cache or a sharded build can emit declarations for a package without downloading its full transitive dependency graph first. If the types are explicit in the source, the build shard has everything it needs.

What Stays the Same

The constraint applies only to exports. Inside a module, life continues as normal. Local variables, private class members, and unexported helper functions can still rely on full type inference. TypeScript will happily infer the type of a loop variable or a closure parameter without complaint.

export function calculateTotal(items: Item[]): number {
  // Local variable: inference is fine
  const taxRate = 0.08;
  
  // Private class member inside a local class: inference is fine
  class Helper {
    private cache = new Map();
  }
  
  return items.reduce((sum, item) => sum + item.price * (1 + taxRate), 0);
}

Only the exported function signature needed an annotation. The internal machinery stays loose and expressive. This keeps the authoring burden tolerable. You are not switching to a fully explicit style everywhere; you are simply formalizing the contract at the boundary of each module.

Is It Right for Your Codebase?

Adopting isolatedDeclarations shifts where you spend your time. You invest a few extra keystrokes when you write an export, and in exchange you stop paying interest on every build. For library authors, this is often an easy sell. Public APIs should probably be annotated anyway. For application developers working inside a closed monorepo, the upfront cost can feel like unnecessary ceremony. But if your team measures build time in coffee breaks, the trade becomes attractive quickly.

You can adopt it incrementally. Enable the flag, run the compiler, and fix the errors it surfaces on exported symbols. The error messages tell you exactly which public-facing types are implicit. Fix those, leave the internals alone, and watch your declaration step accelerate.

One thing to remember: this flag does not make TypeScript's type checker itself faster. If you want quicker feedback in your editor or faster tsc --noEmit runs, you still need project references, stricter file inclusion, or other architectural fixes. isolatedDeclarations specifically targets the emission of .d.ts files. It is a build optimization, not a type-checking optimization.

The Real Takeaway

isolatedDeclarations asks you to treat your public types as first-class artifacts. Stop making the compiler deduce them. Write them down. Once you do, the compiler stops crawling through your entire dependency graph every time it needs to generate a declaration file. It emits in parallel, tools like esbuild and swc handle full TypeScript workflows, and your monorepo builds stop dragging.

The cost moves from build time to authoring time. For most growing teams, that is a trade worth making.