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.
این موضوع ابزارهایی را که میتوانید استفاده کنید نیز تغییر میدهد. ترنسپایلرهایی مانند esbuild و swc در حال حاضر در تبدیل TypeScript به JavaScript بسیار سریع هستند، اما بسیاری از تیمها هنوز tsc را بهطور جداگانه فقط برای تولید فایلهای .d.ts اجرا میکنند. با استفاده از isolatedDeclarations آن ابزارهای سریع میتوانند هر دو وظیفه را انجام دهند. آنها نیازی به بازسازی کل سیستم تایپ TypeScript برای تولید اعلانها (declarations) ندارند؛ آنها فقط نیاز دارند نحو (syntax) را تجزیه کرده و تایپهای صریحی را که ارائه کردهاید کپی کنند. این امر باعث میشود بیلدهای end-to-end در TypeScript با زنجیرههای ابزار جایگزین، بسیار عملیتر شوند.
بیلدهای توزیعشده (distributed) و افزایشی (incremental) نیز سادهتر میشوند. در یک فرآیند CI، یک کش از راه دور یا یک بیلد تکهتکه شده (sharded build) میتواند اعلانها را برای یک پکیج، بدون اینکه ابتدا کل گراف وابستگیهای گذرا (transitive dependency graph) آن را دانلود کند، منتشر کند. اگر تایپها در منبع صریح باشند، بخش بیلد تمام آنچه را که نیاز دارد در اختیار دارد.
آنچه بدون تغییر باقی میماند
این محدودیت فقط برای exports اعمال میشود. در داخل یک ماژول، همه چیز طبق روال عادی پیش میرود. متغیرهای محلی، اعضای خصوصی کلاس و توابع کمکی که اکسپورت نشدهاند، همچنان میتوانند بر استنتاج کامل تایپ (full type inference) تکیه کنند. TypeScript بدون هیچ شکایتی، تایپ یک متغیر حلقه یا یک پارامتر closure را استنتاج خواهد کرد.
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);
}
تنها امضای تابع اکسپورت شده نیاز به آنوتیشن (annotation) داشت. مکانیزمهای داخلی همچنان آزاد و منعطف باقی میمانند. این کار باعث میشود بار نویسندگی (authoring burden) قابل تحمل باشد. شما در حال تغییر به یک سبک کاملاً صریح در همه جا نیستید؛ بلکه صرفاً در حال رسمی کردن قرارداد در مرز هر ماژول هستید.
آیا این برای کدبیس شما مناسب است؟
پذیرش isolatedDeclarations محل صرف زمان شما را تغییر میدهد. شما هنگام نوشتن یک export چند کلید اضافه فشار میدهید و در عوض، دیگر بابت هر بیلد، هزینهی سنگینی (به شکل زمان) پرداخت نمیکنید. برای نویسندگان کتابخانه، این اغلب یک پیشنهاد قابل قبول است. APIهای عمومی احتمالاً در هر صورت باید دارای آنوتیشن باشند. برای توسعهدهندگان اپلیکیشن که در یک monorepo بسته کار میکنند، هزینه اولیه میتواند مانند یک تشریفات غیرضروری به نظر برسد. اما اگر تیم شما زمان بیلد را با استراحتهای کوتاه قهوه میسنجد، این معامله به سرعت جذاب میشود.
میتوانید آن را به صورت تدریجی اتخاذ کنید. پرچم (flag) را فعال کنید، کامپایلر را اجرا کنید و خطاهایی را که در نمادهای (symbols) اکسپورت شده ظاهر میشوند، اصلاح کنید. پیامهای خطا دقیقاً به شما میگویند کدام تایپهای عمومی به صورت ضمنی (implicit) هستند. آنها را اصلاح کنید، به بخشهای داخلی کاری نداشته باشید و سرعت مرحله تولید اعلانها را مشاهده کنید.
یک نکته که باید به خاطر داشت: این پرچم باعث سریعتر شدن خودِ تایپچکر (type checker) در TypeScript نمیشود. اگر میخواهید بازخورد سریعتری در ادیتور خود یا اجرای سریعتر tsc --noEmit داشته باشید، همچنان به project references، گنجاندن دقیقتر فایلها یا سایر اصلاحات معماری نیاز دارید. isolatedDeclarations مشخصاً تولید فایلهای .d.ts را هدف قرار میدهد. این یک بهینهسازی بیلد است، نه یک بهینهسازی تایپچکینگ.
نتیجهگیری اصلی
isolatedDeclarations از شما میخواهد که با تایپهای عمومی خود مانند مصنوعات درجه اول (first-class artifacts) رفتار کنید. از وادار کردن کامپایلر به استنتاج آنها دست بردارید. آنها را بنویسید. وقتی این کار را انجام دهید، کامپایلر دیگر لازم نیست هر بار که نیاز به تولید یک فایل اعلان دارد، در کل گراف وابستگی شما جستجو کند. کامپایلر به صورت موازی عمل میکند، ابزارهایی مانند esbuild و swc جریانهای کاری کامل TypeScript را مدیریت میکنند و بیلدهای monorepo شما دیگر کند نخواهند بود.
هزینه از زمان بیلد به زمان نویسندگی منتقل میشود. برای اکثر تیمهای در حال رشد، این معاملهای است که ارزش انجام دادن دارد.
