Projekty TypeScript rosną. Liczba plików się mnoży. Zależności się plączą. W końcu proces budowania napotyka ścianę, która nie ma nic wspólnego ze złożonością logiki, a wszystko z faktem, że kompilator musi przeczytać cały wszechświat, zanim będzie mógł zapisać choćby jeden plik deklaracji.
TypeScript 6.0 rozwiązuje ten problem za pomocą isolatedDeclarations. Ta funkcja zmienia sposób, w jaki powstają pliki .d.ts. Zamiast wiązać generowanie deklaracji z pełnym potokiem sprawdzania typów (type-checking pipeline), pozwala kompilatorowi generować te pliki poprzez analizę każdego pliku źródłowego w izolacji. Wynikiem jest proces budowania, który może przebiegać równolegle dla tysięcy plików, zamiast mozolnie przeszukiwać graf zależności link po linku.
Prawdziwe wąskie gardło
Obecnie generowanie plików deklaracji jest operacją szeregową. Gdy włączysz --declaration i uruchomisz kompilator, TypeScript nie może wygenerować pliku .d.ts dla danego modułu, dopóki w pełni nie zrozumie każdego typu, którego ten moduł dotyka. Jeśli utils.ts importuje typy z types.ts, a types.ts pobiera coś z api.ts, kompilator musi rozwiązać ten łańcuch, zanim będzie mógł opisać, co eksportuje utils.ts.
W dużym monorepo ta kaskada jest brutalna. Pojedynczy plik znajdujący się blisko korzenia grafu importów może blokować generowanie deklaracji dla setek plików zależnych. Twój procesor ma osiem rdzeni, ale siedem z nich pozostaje bezczynnych, podczas gdy TypeScript mozolnie rekonstruuje kształt każdego interfejsu na granicach pakietów. Kompilator wykonuje niezbędną pracę, ale sprzężenie między sprawdzaniem typów a generowaniem deklaracji oznacza, że ponosisz pełny koszt analizy międzyplikowej, nawet jeśli chcesz jedynie zapisać na dysku typy publicznego interfejsu.
Jak isolatedDeclarations zmienia zasady
isolatedDeclarations przełamuje to sprzężenie. Gdy flaga jest włączona, kompilator zgadza się wygenerować plik .d.ts dla pliku źródłowego bez pytania jakiegokolwiek innego pliku o znaczenie czegokolwiek. Robi to, wymagając prostego kontraktu: każdy eksportowany symbol musi posiadać jawną, widoczną adnotację typu w miejscu, w którym został zadeklarowany.
Jeśli kompilator widzi pełny typ zapisany bezpośrednio w kodzie źródłowym, nie musi przeprowadzać wnioskowania (inference). Nie musi śledzić importów. Nie musi wiedzieć, czy identyfikator User w innym pliku jest interfejsem, aliasem typu czy klasą. Po prostu generuje dokładnie to, co napisałeś.
Oznacza to, że plik A i plik B mogą generować swoje deklaracje jednocześnie. Orkiestrator budowania może przekazać każdy plik do osobnego wątku. Szybkie transpilatory, które wcześniej pomijały generowanie .d.ts z powodu braku pełnego sprawdzania typów, mogą teraz również tworzyć pliki deklaracji, ponieważ praca ta staje się czysto syntaktyczna.
Koszt: Zapisz to wyraźnie
Szybkość nie jest za darmo. Musisz przestać polegać na wnioskowaniu typów (type inference) w przypadku wszystkiego, co eksportujesz. Każda publiczna funkcja, klasa, zmienna i stała musi mieć swój typ wyraźnie określony. Jeśli TypeScript będzie musiał obliczyć typ, analizując instrukcję return lub rozwiązując argument generyczny, isolatedDeclarations zgłosi błąd.
Oto jak wygląda to w praktyce. Bez flagi możesz napisać:
export function fetchUser(id: number) {
return fetch(`/users/${id}`).then(r => r.json());
}
TypeScript wnioskuje typ zwracany, analizując fetch, następnie Promise.prototype.then, a potem anonimową funkcję zwracającą r.json(). Aby wygenerować .d.ts, kompilator musi przeprowadzić całą tę analizę.
Po włączeniu isolatedDeclarations musisz dodać adnotację do eksportu:
interface User {
id: number;
email: string;
}
export function fetchUser(id: number): Promise<User> {
return fetch(`/users/${id}`).then(r => r.json());
}
Teraz kompilator natychmiast widzi Promise<User>. Generuje deklarację i idzie dalej.
Ta zasada ma szerokie zastosowanie. Eksportowane tablice wymagają jawnych typów zamiast wnioskowania na podstawie ich elementów. Eksportowane obiekty wymagają jawnych adnotacji typów, jeśli ich kształt ma znaczenie dla konsumentów. Funkcje generyczne wymagają, aby ich typy zwracane i ograniczenia (constraints) były widoczne w miejscu deklaracji. Nie możesz wyeksportować wyniku złożonego typu mapowanego (mapped type) bez nadania mu nazwanego aliasu typu, który jest w pełni wypisany.
Plus polega na tym, że Twoje publiczne API staje się samodokumentujące. Konsumenci — i kompilator — nie muszą już odtwarzać Twoich intencji na podstawie szczegółów implementacji. Typy stają się świadomym kontraktem.
Gdzie zyskujemy czas
W dużym kodzie źródłowym wpływ jest natychmiastowy. Czas budowania, który rozciągał się do minut, może spaść do sekund, ponieważ generowanie deklaracji przestaje być głównym wąskim gardłem. Każdy plik generuje się niezależnie, więc proces skaluje się wraz z liczbą posiadanych rdzeni, a nie wraz z głębokością grafu importów.
To zmienia również narzędzia, których możesz używać. Transpilatory takie jak esbuild i swc są już błyskawiczne w zamianie TypeScript na JavaScript, ale wiele zespołów wciąż uruchamia tsc oddzielnie tylko po to, by wygenerować pliki .d.ts. Dzięki isolatedDeclarations te szybkie narzędzia mogą wykonywać obie te czynności. Nie muszą one replikować całego systemu typów TypeScript, aby generować deklaracje; wystarczy im analiza składni i skopiowanie podanych przez Ciebie jawnych typów. Dzięki temu budowanie projektów TypeScript od początku do końca przy użyciu alternatywnych zestawów narzędzi staje się znacznie bardziej wykonalne.
Uproszczone zostają również budowania rozproszone i przyrostowe. W ciągłej integracji, pamięć podręczna (remote cache) lub budowanie shardowane może emitować deklaracje dla pakietu bez konieczności wcześniejszego pobierania całego grafu zależności przechodnich. Jeśli typy w kodzie źródłowym są jawne, shard budujący posiada wszystko, czego potrzebuje.
Co pozostaje bez zmian
Ograniczenie dotyczy tylko eksportów. Wewnątrz modułu wszystko działa normalnie. Lokalne zmienne, prywatne członkowie klas i nieeksportowane funkcje pomocnicze nadal mogą polegać na pełnej inferencji typów. TypeScript bez problemu wywnioskuje typ zmiennej pętli lub parametru domknięcia bez żadnych zastrzeżeń.
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);
}
Tylko sygnatura eksportowanej funkcji wymagała adnotacji. Wewnętrzna mechanika pozostaje swobodna i ekspresyjna. Dzięki temu ciężar pisania kodu pozostaje na akceptowalnym poziomie. Nie przechodzisz na w pełni jawny styl w każdym miejscu; po prostu formalizujesz kontrakt na granicy każdego modułu.
Czy to rozwiązanie jest odpowiednie dla Twojej bazy kodu?
Wdrożenie isolatedDeclarations zmienia sposób, w jaki zarządzasz czasem. Inwestujesz kilka dodatkowych uderzeń w klawisze podczas pisania eksportu, a w zamian przestajesz „płacić odsetki” przy każdym budowaniu. Dla twórców bibliotek jest to często łatwa decyzja. Publiczne API i tak prawdopodobnie powinny być anotowane. Dla programistów aplikacji pracujących w zamkniętym monorepo, koszt początkowy może wydawać się zbędną formalnością. Jeśli jednak Twój zespół mierzy czas budowania w przerwach na kawę, wymiana ta szybko staje się atrakcyjna.
Możesz wdrażać to stopniowo. Włącz flagę, uruchom kompilator i napraw błędy, które pojawią się przy eksportowanych symbolach. Komunikaty o błędach dokładnie wskażą, które typy publiczne są niejawne. Napraw je, zostaw wnętrze kodu w spokoju i obserwuj, jak przyspiesza etap generowania deklaracji.
Warto pamiętać o jednym: ta flaga nie przyspiesza samego sprawdzania typów w TypeScript. Jeśli chcesz szybszej informacji zwrotnej w edytorze lub szybszego działania tsc --noEmit, nadal potrzebujesz referencji projektowych, ściślejszego dołączania plików lub innych poprawek architektonicznych. isolatedDeclarations celuje konkretnie w generowanie plików .d.ts. Jest to optymalizacja budowania, a nie optymalizacja sprawdzania typów.
Najważniejszy wniosek
isolatedDeclarations wymaga od Ciebie traktowania publicznych typów jako obiektów pierwszej klasy. Przestań zmuszać kompilator do ich wnioskowania. Po prostu je zapisz. Gdy to zrobisz, kompilator przestanie przeszukiwać cały graf zależności za każdym razem, gdy musi wygenerować plik deklaracji. Generowanie odbywa się równolegle, narzędzia takie jak esbuild i swc obsługują pełne przepływy pracy TypeScript, a budowanie monorepo przestaje się przeciągać.
Koszt przesuwa się z czasu budowania na czas pisania kodu. Dla większości rozwijających się zespołów jest to wymiana warta podjęcia.
