Menu

declare in TypeScript: i file di dichiarazione .d.ts spiegati

Un file .d.ts descrive i tipi di codice JavaScript senza contenerne nemmeno una riga, e la parola chiave declare fa lo stesso dentro un file .ts. Scopri come vengono generati i file di dichiarazione, a cosa servono i pacchetti @types, come tipizzare un modulo senza tipi e come declare global e la module augmentation estendono i tipi esistenti.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un file di dichiarazione (.d.ts) contiene tipi e nient'altro: firme, interfacce e forme di classi per del JavaScript che vive altrove. La parola chiave declare fa lo stesso lavoro dentro un file normale. Dice al compilatore "questo esiste a runtime, fidati", e non emette codice.

Il compilatore ha accettato __APP_VERSION__.length perché la dichiarazione dice che è una stringa. A runtime quella variabile non esiste, quindi leggere .length lancia ReferenceError: __APP_VERSION__ is not defined, un'eccezione a runtime che il compilatore non ha mai visto arrivare. È il contratto di ogni dichiarazione: i tipi sono veri solo quanto il JavaScript che c'è dietro.

Cosa c'è in un file .d.ts

Compilare con declaration: true mostra bene l'idea. Da questo sorgente:

// price.ts
export interface LineItem {
    name: string;
    price: number;
    qty: number;
}

const TAX = 0.2;

export function total(items: LineItem[]) {
    const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    return Math.round(sum * (1 + TAX) * 100) / 100;
}

export class Cart {
    private items: LineItem[] = [];
    add(item: LineItem) {
        this.items.push(item);
        return this;
    }
}

tsc scrive price.js e questo price.d.ts:

export interface LineItem {
    name: string;
    price: number;
    qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
    private items;
    add(item: LineItem): this;
}

I corpi delle funzioni spariscono, i tipi di ritorno dedotti vengono scritti per esteso (number, this), il campo privato mantiene il nome ma perde il tipo, e il TAX non esportato non c'è proprio. Una libreria pubblica il .js per Node e il .d.ts per il tuo editor e compilatore. emitDeclarationOnly: true produce solo i file .d.ts, per i progetti in cui è un bundler a produrre il JavaScript.

Anche gli oggetti predefiniti di ES2022 che usi ogni giorno vengono da file di dichiarazione: lib.es2022.d.ts e compagni sono inclusi in TypeScript e vengono selezionati da target e lib.

Le forme di declare

Ogni istruzione declare descrive qualcosa che esiste già. In un file .d.ts senza import né export (un file di dichiarazione globale), ognuna di queste diventa visibile a tutto il progetto:

// globals.d.ts
declare const API_URL: string;                     // a global constant
declare let debugMode: boolean;                    // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget {                             // a class from a script tag
    constructor(el: string);
    render(): void;
}
declare namespace Analytics {                      // a global object
    function page(name: string): void;
}
declare module "legacy-charts" {                   // a module you import
    export function draw(data: number[]): void;
}

Dentro un file .d.ts, interface e type non hanno bisogno di declare; qualsiasi altra dichiarazione top-level senza declare o export è l'errore TS1046. Appena un file ha un import o un export, le sue dichiarazioni diventano locali, e le aggiunte allo scope globale vanno in un blocco declare global (mostrato più sotto). La firma dichiarata è ciò con cui il compilatore confronta le chiamate:

Il compilatore segnala index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Passa { items: "3" }, o cambia la dichiarazione se la funzione reale accetta numeri.

Pacchetti @types

Molti pacchetti npm includono i propri file .d.ts, indicati dal campo types (o da una condizione types in exports) del loro package.json. Per i pacchetti solo JavaScript, il progetto DefinitelyTyped, mantenuto dalla comunità, pubblica i tipi sotto lo scope @types:

npm i lodash
npm i -D @types/lodash

Quando fai import di un pacchetto, TypeScript cerca automaticamente node_modules/@types/{name}. I tipi che descrivono globali invece di un import, come @types/node (process, Buffer) o i describe e it di un test runner, vanno elencati in tsconfig.json:

{
    "compilerOptions": {
        "types": ["node"]
    }
}

Senza quella voce, TypeScript 7 non li carica, anche quando il pacchetto è installato:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

tsc --init scrive "types": [] nella nuova configurazione, con un commento che suggerisce ["node"] per i progetti Node.

Tipizzare un modulo senza tipi

Importare un pacchetto JavaScript che non ha tipi, né inclusi né in @types, è l'errore TS7016:

error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.

Risolvi con un file .d.ts in qualsiasi punto del progetto (una cartella types/ è comune; basta che sia coperta da include) senza import né export top-level. Descrivi le parti che usi:

// types/fakelib.d.ts
declare module "fakelib" {
    export function hi(name: string): string;
    export const version: string;
}

// Non-code files a bundler lets you import
declare module "*.svg" {
    const url: string;
    export default url;
}

La versione più corta, declare module "fakelib"; su una riga, rende any ogni import dal pacchetto. Elimina l'errore e con lui ogni controllo, quindi consideralo un passaggio temporaneo.

declare global

Il codice che aggiunge elementi allo scope globale, come un polyfill, un nuovo metodo su un oggetto predefinito o una globale impostata da un tag script, ha bisogno dei tipi corrispondenti. declare global li aggiunge. Deve stare in un modulo (un file con un import o un export), ed è per questo che export {} è in cima:

interface Array<T> si fonde con l'interfaccia Array predefinita invece di sostituirla, e var (non let o const) è ciò che aggiunge una proprietà a globalThis. Estendere i prototipi predefiniti è rischioso nel codice condiviso; la stessa tecnica declare global è il modo in cui i progetti aggiungono le proprie variabili a process.env nell'interfaccia NodeJS.ProcessEnv di @types/node.

Module augmentation

Per aggiungere qualcosa ai tipi di un pacchetto che importi, ridichiara il nome del suo modulo e riapri l'interfaccia. Il file deve essere a sua volta un modulo (ci pensa l'import; funziona anche export {}). Senza un import o un export, lo stesso blocco dichiara un modulo config-lib completamente nuovo che nasconde i tipi reali del pacchetto:

// types/config-lib.d.ts
import "config-lib";

declare module "config-lib" {
    interface Settings {
        beta: boolean; // merged into the package's own Settings interface
    }
}

Dopo questo, load().beta da config-lib è tipizzato boolean ovunque. Le interfacce si fondono; i type alias no, quindi una libreria deve esportare un'interface perché questo funzioni. È così che i plugin aggiungono campi agli oggetti request o config di un framework.

skipLibCheck

skipLibCheck: true impedisce al compilatore di controllare i tipi dei file .d.ts, compresi quelli in node_modules. Il tuo codice viene comunque controllato rispetto a essi. Fa risparmiare tempo ed evita errori dovuti a due pacchetti con dichiarazioni in disaccordo, ed è per questo che tsc --init lo attiva. Il costo è che nemmeno un errore dentro i tuoi file .d.ts viene segnalato.

Domande frequenti

Che cos'è un file .d.ts in TypeScript?

Un file di dichiarazione: contiene solo tipi (firme di funzioni, interfacce, forme di classi) e nessuna implementazione. Descrive del JavaScript che esiste altrove, come una libreria compilata o le API integrate del browser, così TypeScript può controllare il codice che lo usa. tsc non emette mai JavaScript per esso.

Cosa fa la parola chiave declare in TypeScript?

Dice al compilatore che un valore esiste a runtime senza crearlo. declare const VERSION: string; ti permette di usare VERSION come stringa, e la riga sparisce dall'output. Se nulla definisce davvero VERSION, il programma fallisce a runtime con un ReferenceError.

Come si risolve "Could not find a declaration file for module"?

L'errore TS7016 significa che il pacchetto non ha tipi. Installali se esistono (npm i -D @types/package-name), oppure aggiungi un file .d.ts con declare module "package-name" { ... } che descriva ciò che usi. declare module "package-name"; da solo zittisce l'errore tipizzando l'intero modulo come any.

Come si generano i file .d.ts da TypeScript?

Imposta "declaration": true in tsconfig.json (o passa --declaration). Ogni file .ts produce allora un .d.ts accanto al suo .js. Aggiungi emitDeclarationOnly quando è un altro strumento a produrre il JavaScript, e declarationMap perché gli editor possano saltare dai tipi al tuo sorgente.

Che differenza c'è tra declare global e declare module?

declare global { ... } aggiunge elementi allo scope globale, per esempio una nuova proprietà su Array o una variabile globale. declare module "name" { ... } descrive un modulo che importi con quel nome. declare global deve stare in un modulo (un file con un import o un export); declare module dichiara un nuovo modulo in un file senza import né export, e ne estende uno esistente dentro un modulo.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA