I progetti TypeScript crescono. I file si moltiplicano. Le dipendenze si intrecciano. E alla fine, la tua build si scontra con un muro che non ha nulla a che fare con la complessità della tua logica, ma tutto a che fare con la necessità del compilatore di leggere l'intero universo prima di poter scrivere un singolo file di dichiarazione.

TypeScript 6.0 affronta questo problema con isolatedDeclarations. Questa funzionalità ripensa il modo in cui nascono i file .d.ts. Invece di legare l'emissione delle dichiarazioni all'intera pipeline di type-checking, permette al compilatore di emettere tali file analizzando ogni file sorgente in isolamento. Il risultato è un processo di build che può essere eseguito in parallelo su migliaia di file, invece di strisciare attraverso il grafo delle dipendenze un collegamento alla volta.

Il vero collo di bottiglia

Attualmente, la generazione dei file di dichiarazione è un'operazione seriale. Quando abiliti --declaration ed esegui il compilatore, TypeScript non può emettere un file .d.ts per un determinato modulo finché non comprende appieno ogni tipo che quel modulo tocca. Se utils.ts importa tipi da types.ts, e types.ts richiama qualcosa da api.ts, il compilatore deve risolvere quella catena prima di poter descrivere ciò che utils.ts esporta.

In un grande monorepo, questa cascata è brutale. Un singolo file vicino alla radice del grafo degli import può bloccare l'emissione delle dichiarazioni per centinaia di file a valle. La tua CPU ha otto core, ma sette rimangono inattivi mentre TypeScript ricostruisce meticolosamente la forma di ogni interfaccia attraverso i confini dei pacchetti. Il compilatore sta svolgendo un lavoro necessario, ma l'accoppiamento tra type checking ed emissione delle dichiarazioni significa che paghi l'intero costo dell'analisi tra i file, anche quando desideri solo che i tipi dell'interfaccia pubblica vengano scritti su disco.

Come isolatedDeclarations cambia le regole

isolatedDeclarations rompe questo accoppiamento. Quando il flag è abilitato, il compilatore accetta di emettere un file .d.ts per un file sorgente senza chiedere a nessun altro file il significato di qualcosa. Lo fa richiedendo un contratto semplice: ogni simbolo esportato deve avere un'annotazione di tipo esplicita e visibile nel punto in cui viene dichiarato.

Se il compilatore può vedere il tipo completo scritto proprio lì nel sorgente, non ha bisogno di eseguire l'inferenza. Non ha bisogno di inseguire gli import. Non ha bisogno di sapere se l'identificatore User in un altro file sia un'interfaccia, un alias di tipo o una classe. Emette semplicemente esattamente ciò che hai scritto.

Ciò significa che il file A e il file B possono generare le loro dichiarazioni simultaneamente. Un orchestratore di build può assegnare ogni file a un thread separato. I transpiler veloci che prima saltavano la generazione di .d.ts perché privi di un compilatore di tipi completo possono ora produrre anche file di dichiarazione, poiché il lavoro diventa puramente sintattico.

Il compromesso: scrivilo esplicitamente

La velocità non è gratuita. Devi smettere di fare affidamento sull'inferenza dei tipi per tutto ciò che esporti. Ogni funzione, classe, variabile e costante pubblica deve avere il proprio tipo esplicitato chiaramente. Se TypeScript deve calcolare il tipo analizzando un'istruzione di ritorno o risolvendo un argomento generico, isolatedDeclarations restituirà un errore.

Ecco come appare in pratica. Senza il flag, potresti scrivere:

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

TypeScript inferisce il tipo di ritorno ispezionando fetch, poi Promise.prototype.then, e infine la funzione anonima che restituisce r.json(). Per emettere un .d.ts, il compilatore deve eseguire tutta questa analisi.

Con isolatedDeclarations abilitato, devi annotare l'export:

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

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

Ora il compilatore vede immediatamente Promise<User>. Emette la dichiarazione e va avanti.

Questa regola si applica ampiamente. Gli array esportati necessitano di tipi espliciti invece di farli inferire dai loro elementi. Gli oggetti esportati necessitano di annotazioni di tipo esplicite se la loro struttura è importante per i consumatori. Le funzioni generiche necessitano che i loro tipi di ritorno e i vincoli siano visibili nel punto di dichiarazione. Non puoi esportare il risultato di un tipo mappato complesso senza assegnargli un alias di tipo nominato che

Questo cambia anche gli strumenti che puoi utilizzare. I transpiler come esbuild e swc sono già velocissimi nel trasformare TypeScript in JavaScript, ma molti team eseguono ancora tsc separatamente solo per produrre i file .d.ts. Con isolatedDeclarations, questi strumenti veloci possono gestire entrambi i compiti. Non hanno bisogno di replicare l'intero sistema di tipi di TypeScript per generare le dichiarazioni; devono solo analizzare la sintassi e copiare i tipi espliciti che hai fornito. Ciò rende molto più fattibili le build TypeScript end-to-end con toolchain alternative.

Anche le build distribuite e incrementali diventano più semplici. Nella continuous integration, una cache remota o una build suddivisa (sharded build) possono emettere le dichiarazioni per un pacchetto senza dover prima scaricare l'intero grafo delle dipendenze transitive. Se i tipi sono espliciti nel codice sorgente, la porzione di build ha tutto ciò di cui ha bisogno.

Cosa rimane invariato

Il vincolo si applica solo alle esportazioni. All'interno di un modulo, tutto continua come al solito. Le variabili locali, i membri privati delle classi e le funzioni helper non esportate possono ancora fare affidamento sulla piena inferenza dei tipi. TypeScript inferirà senza problemi il tipo di una variabile di un ciclo o di un parametro di una closure, senza lamentarsi.

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

Solo la firma della funzione esportata necessitava di un'annotazione. La meccanica interna rimane flessibile ed espressiva. Questo mantiene il carico di scrittura tollerabile. Non stai passando a uno stile completamente esplicito ovunque; stai semplicemente formalizzando il contratto al confine di ogni modulo.

È adatto al tuo codebase?

L'adozione di isolatedDeclarations sposta il modo in cui spendi il tuo tempo. Investi qualche tasto in più quando scrivi un'esportazione e, in cambio, smetti di pagare "interessi" su ogni build. Per gli autori di librerie, questo è spesso un argomento facile da vendere. Le API pubbliche dovrebbero probabilmente essere annotate in ogni caso. Per gli sviluppatori di applicazioni che lavorano all'interno di un monorepo chiuso, il costo iniziale può sembrare una formalità non necessaria. Ma se il tuo team misura il tempo di build in pause caffè, il compromesso diventa rapidamente attraente.

Puoi adottarlo incrementalmente. Abilita il flag, esegui il compilatore e correggi gli errori che segnala sui simboli esportati. I messaggi di errore ti dicono esattamente quali tipi esposti verso l'esterno sono impliciti. Correggili, lascia stare le parti interne e guarda accelerare la fase di generazione delle dichiarazioni.

Una cosa da ricordare: questo flag non rende più veloce il type checker di TypeScript stesso. Se desideri un feedback più rapido nel tuo editor o esecuzioni più veloci di tsc --noEmit, avrai comunque bisogno di project references, un'inclusione dei file più rigorosa o altri interventi architettonici. isolatedDeclarations mira specificamente all'emissione dei file .d.ts. È un'ottimizzazione della build, non un'ottimizzazione del type-checking.

La vera conclusione

isolatedDeclarations ti chiede di trattare i tuoi tipi pubblici come artefatti di prima classe. Smetti di farli dedurre al compilatore. Scrivili. Una volta fatto, il compilatore smetterà di scansionare l'intero grafo delle dipendenze ogni volta che deve generare un file di dichiarazione. Emette in parallelo, strumenti come esbuild e swc gestiscono interi workflow TypeScript e le build del tuo monorepo smettono di trascinarsi.

Il costo si sposta dal tempo di build al tempo di scrittura. Per la maggior parte dei team in crescita, è un compromesso che vale la pena fare.