TypeScript projects grow. Files multiply. Dependencies tangle. And eventually, your build hits a wall that has nothing to do with the complexity of your logic and everything to do with the compiler needing to read the entire universe before it can write a single declaration file.

TypeScript 6.0 addresses this with isolatedDeclarations. The feature rethinks how .d.ts files are born. Instead of tying declaration emission to the full type-checking pipeline, it lets the compiler emit those files by looking at each source file in isolation. The result is a build process that can run in parallel across thousands of files rather than crawling through your dependency graph one link at a time.

The Real Bottleneck

Right now, generating declaration files is a serial operation. When you enable --declaration and run the compiler, TypeScript cannot emit a .d.ts file for a given module until it fully understands every type that module touches. If utils.ts imports types from types.ts, and types.ts pulls in something from api.ts, the compiler must resolve that chain before it can describe what utils.ts exports.

In a large monorepo, this cascade is brutal. A single file near the root of your import graph can block declaration emission for hundreds of downstream files. Your CPU has eight cores, but seven of them sit idle while TypeScript painstakingly reconstructs the shape of every interface across package boundaries. The compiler is doing necessary work, but the coupling between type checking and declaration emission means you pay the full cost of cross-file analysis even when you only want the public surface types written to disk.

How IsolatedDeclarations Changes the Rules

isolatedDeclarations breaks that coupling. When the flag is enabled, the compiler agrees to emit a .d.ts file for a source file without asking any other file what anything means. It does this by requiring a simple contract: every exported symbol must carry an explicit, visible type annotation at the site where it is declared.

If the compiler can see the full type written right there in the source, it does not need to perform inference. It does not need to chase imports. It does not need to know whether the identifier User in another file is an interface, a type alias, or a class. It simply emits exactly what you wrote.

This means file A and file B can generate their declarations simultaneously. A build orchestrator can hand each file to a separate thread. Fast transpilers that previously skipped .d.ts generation because they lacked a full type checker can now produce declaration files too, because the work becomes purely syntactic.

The Tradeoff: Write It Down

Speed does not come free. You must stop relying on type inference for anything you export. Every public function, class, variable, and constant needs its type spelled out explicitly. If TypeScript has to compute the type by looking at a return statement or resolving a generic argument, isolatedDeclarations will error.

Here is what that looks like in practice. Without the flag, you might write:

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

TypeScript infers the return type by inspecting fetch, then Promise.prototype.then, then the anonymous function returning r.json(). To emit a .d.ts, the compiler needs to perform all of that analysis.

With isolatedDeclarations enabled, you must annotate the export:

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

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

Now the compiler sees Promise<User> immediately. It emits the declaration and moves on.

This rule applies broadly. Exported arrays need explicit types instead of having them inferred from their elements. Exported objects need explicit type annotations if their shape matters to consumers. Generic functions need their return types and constraints visible at the declaration site. You cannot export the result of a complex mapped type without giving it a named type alias that is written out in full.

The upside is that your public API becomes self-documenting. Consumers—and the compiler—no longer have to reverse-engineer your intent from implementation details. The types are a deliberate contract.

Where the Time Goes

In a large codebase, the impact is immediate. Build times that stretch into minutes can drop to seconds because declaration emission stops being the long pole in the tent. Each file emits independently, so the process scales with the number of cores you have, not with the depth of your import graph.

Điều này cũng thay đổi những công cụ mà bạn có thể sử dụng. Các trình transpiler như esbuild và swc vốn đã cực kỳ nhanh trong việc chuyển đổi TypeScript thành JavaScript, nhưng nhiều đội ngũ vẫn chạy tsc riêng biệt chỉ để tạo ra các tệp .d.ts. Với isolatedDeclarations, các công cụ nhanh chóng đó có thể đảm nhận cả hai nhiệm vụ. Chúng không cần phải tái lập toàn bộ hệ thống kiểu của TypeScript để tạo ra các khai báo; chúng chỉ cần phân tích cú pháp và sao chép các kiểu tường minh mà bạn đã cung cấp. Điều đó giúp cho việc build TypeScript đầu-cuối với các chuỗi công cụ (toolchains) thay thế trở nên khả thi hơn nhiều.

Việc build phân tán và tăng dần cũng trở nên đơn giản hơn. Trong tích hợp liên tục (continuous integration), một bộ nhớ đệm từ xa (remote cache) hoặc một bản build phân mảnh (sharded build) có thể xuất ra các khai báo cho một gói mà không cần tải xuống toàn bộ đồ thị phụ thuộc bắc cầu trước đó. Nếu các kiểu dữ liệu được khai báo tường minh trong mã nguồn, phần build phân mảnh đó sẽ có mọi thứ nó cần.

Những gì vẫn giữ nguyên

Ràng buộc này chỉ áp dụng cho các thành phần được export. Bên trong một module, mọi thứ vẫn diễn ra bình thường. Các biến cục bộ, các thành viên private của class và các hàm bổ trợ không được export vẫn có thể dựa vào việc suy luận kiểu đầy đủ. TypeScript sẽ vui vẻ suy luận kiểu của một biến vòng lặp hoặc một tham số closure mà không gặp trở ngại nào.

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);
}

Chỉ có chữ ký hàm được export là cần một annotation. Các cơ chế bên trong vẫn linh hoạt và giàu tính biểu đạt. Điều này giúp gánh nặng khi viết mã vẫn ở mức có thể chấp nhận được. Bạn không chuyển sang phong cách hoàn toàn tường minh ở mọi nơi; bạn chỉ đơn giản là đang chính thức hóa các hợp đồng (contract) tại ranh giới của mỗi module.

Nó có phù hợp với codebase của bạn không?

Việc áp dụng isolatedDeclarations làm thay đổi cách bạn phân bổ thời gian. Bạn đầu tư thêm một vài thao tác gõ phím khi viết một lệnh export, và đổi lại, bạn không còn phải trả "lãi suất" cho mỗi lần build nữa. Đối với những người viết thư viện, đây thường là một lựa chọn dễ dàng được chấp nhận. Dù sao thì các Public API cũng nên được annotate. Đối với các nhà phát triển ứng dụng làm việc trong một monorepo đóng, chi phí ban đầu có thể cảm thấy như một sự rườm rà không cần thiết. Nhưng nếu đội ngũ của bạn đo lường thời gian build bằng những giờ nghỉ giải lao uống cà phê, thì sự đánh đổi này sẽ nhanh chóng trở nên hấp dẫn.

Bạn có thể áp dụng nó một cách dần dần. Bật flag lên, chạy trình biên dịch, và sửa các lỗi mà nó hiển thị trên các symbol được export. Các thông báo lỗi sẽ cho bạn biết chính xác kiểu dữ liệu nào đang hiển thị ra bên ngoài là kiểu ngầm định. Hãy sửa chúng, để yên các phần bên trong, và chứng kiến bước tạo khai báo của bạn tăng tốc.

Một điều cần nhớ: flag này không làm cho chính trình kiểm tra kiểu của TypeScript chạy nhanh hơn. Nếu bạn muốn nhận phản hồi nhanh hơn trong trình soạn thảo hoặc chạy tsc --noEmit nhanh hơn, bạn vẫn cần đến project references, việc bao gồm tệp nghiêm ngặt hơn, hoặc các giải pháp kiến trúc khác. isolatedDeclarations nhắm mục tiêu cụ thể vào việc tạo ra (emission) các tệp .d.ts. Nó là một sự tối ưu hóa quá trình build, không phải là tối ưu hóa việc kiểm tra kiểu.

Điểm mấu chốt thực sự

isolatedDeclarations yêu cầu bạn đối xử với các kiểu dữ liệu công khai như những thành phần hạng nhất (first-class artifacts). Đừng bắt trình biên dịch phải suy luận chúng nữa. Hãy viết chúng ra. Một khi bạn làm vậy, trình biên dịch sẽ ngừng việc phải dò tìm qua toàn bộ đồ thị phụ thuộc mỗi khi nó cần tạo một tệp khai báo. Nó sẽ thực hiện việc emit song song, các công cụ như esbuild và swc có thể xử lý toàn bộ quy trình làm việc với TypeScript, và các bản build monorepo của bạn sẽ không còn bị trì trệ nữa.

Chi phí được chuyển dịch từ thời gian build sang thời gian viết mã. Đối với hầu hết các đội ngũ đang phát triển, đó là một sự đánh đổi xứng đáng.