Menu

Declare en TypeScript : les fichiers de déclaration .d.ts

Un fichier .d.ts décrit les types d'un code JavaScript sans en contenir une ligne, et le mot clé declare fait la même chose dans un fichier .ts. Découvrez comment les fichiers de déclaration sont générés, le rôle des paquets @types, comment typer un module sans types, et comment declare global et l'augmentation de module étendent des types existants.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un fichier de déclaration (.d.ts) contient des types et rien d'autre : des signatures, des interfaces et des formes de classes pour du JavaScript qui se trouve ailleurs. Le mot clé declare fait le même travail dans un fichier ordinaire. Il dit au compilateur « ceci existe à l'exécution, fais-moi confiance », et n'émet aucun code.

Le compilateur a accepté __APP_VERSION__.length parce que la déclaration dit que c'est une chaîne. À l'exécution, aucune variable de ce nom n'existe, donc lire .length lève ReferenceError: __APP_VERSION__ is not defined, une exception à l'exécution que le compilateur n'a jamais vue venir. C'est le contrat de toute déclaration : les types ne sont vrais que si le JavaScript derrière eux l'est.

Ce que contient un fichier .d.ts

Compiler avec declaration: true montre le mieux l'idée. À partir de cette source :

// 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 écrit price.js et ce 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;
}

Les corps de fonctions ont disparu, les types de retour déduits sont écrits en toutes lettres (number, this), le champ privé garde son nom mais perd son type, et le TAX non exporté n'y figure pas du tout. Une bibliothèque publie le .js pour Node et le .d.ts pour votre éditeur et votre compilateur. emitDeclarationOnly: true ne produit que les fichiers .d.ts, pour les projets où un bundler construit le JavaScript.

Les objets intégrés d'ES2022 que vous utilisez tous les jours viennent aussi de fichiers de déclaration : lib.es2022.d.ts et ses voisins sont livrés avec TypeScript et sont sélectionnés par target et lib.

Les formes de declare

Chaque instruction declare décrit quelque chose qui existe déjà. Dans un fichier .d.ts sans import ni export (un fichier de déclaration global), chacune de ces formes devient visible dans tout le projet :

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

Dans un fichier .d.ts, interface et type n'ont pas besoin de declare ; toute autre déclaration de premier niveau sans declare ni export provoque l'erreur TS1046. Dès qu'un fichier contient un import ou un export, ses déclarations lui sont locales, et les ajouts à la portée globale vont dans un bloc declare global (montré plus bas). La signature déclarée est ce contre quoi le compilateur vérifie les appels :

Le compilateur signale index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Passez { items: "3" }, ou modifiez la déclaration si la vraie fonction accepte des nombres.

Les paquets @types

Beaucoup de paquets npm livrent leurs propres fichiers .d.ts, référencés par le champ types (ou une condition types dans exports) de leur package.json. Pour les paquets uniquement JavaScript, le projet communautaire DefinitelyTyped publie des types sous le scope @types :

npm i lodash
npm i -D @types/lodash

Quand vous importez un paquet avec import, TypeScript cherche automatiquement node_modules/@types/{name}. Les types qui décrivent des globales plutôt qu'un import, comme @types/node (process, Buffer) ou les describe et it d'un lanceur de tests, doivent être listés dans tsconfig.json :

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

Sans cette entrée, TypeScript 7 ne les charge pas, même quand le paquet est installé :

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 écrit "types": [] dans la nouvelle configuration, avec un commentaire qui suggère ["node"] pour les projets Node.

Typer un module sans types

Importer un paquet JavaScript qui n'a pas de types, ni intégrés ni dans @types, provoque l'erreur TS7016 :

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

Corrigez-la avec un fichier .d.ts placé n'importe où dans votre projet (un dossier types/ est courant ; il doit seulement être couvert par include) et sans import ni export de premier niveau. Décrivez les parties que vous utilisez :

// 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 version la plus courte, declare module "fakelib"; sur une ligne, rend any chaque import venant du paquet. Elle supprime l'erreur et toutes les vérifications avec elle : considérez-la comme une étape temporaire.

declare global

Le code qui ajoute des éléments à la portée globale, comme un polyfill, une nouvelle méthode sur un objet intégré ou une globale définie par une balise script, a besoin des types correspondants. declare global les ajoute. Il doit se trouver dans un module (un fichier avec un import ou un export), c'est pourquoi export {} figure en haut :

interface Array<T> fusionne avec l'interface Array intégrée au lieu de la remplacer, et c'est var (pas let ni const) qui ajoute une propriété à globalThis. Étendre les prototypes intégrés est risqué dans du code partagé ; la même technique declare global est celle qu'utilisent les projets pour ajouter leurs propres variables à process.env dans l'interface NodeJS.ProcessEnv de @types/node.

Augmentation de module

Pour compléter les types d'un paquet que vous importez, redéclarez son nom de module et rouvrez l'interface. Le fichier doit lui-même être un module (l'import s'en charge ; export {} fonctionne aussi). Sans import ni export, le même bloc déclare un tout nouveau module config-lib qui masque les vrais types du paquet :

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

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

Après cela, load().beta venant de config-lib est typé boolean partout. Les interfaces fusionnent, pas les alias de type : une bibliothèque doit donc exporter une interface pour que cela fonctionne. C'est ainsi que les plugins ajoutent des champs aux objets de requête ou de configuration d'un framework.

skipLibCheck

skipLibCheck: true empêche le compilateur de vérifier les types des fichiers .d.ts, y compris ceux de node_modules. Votre propre code reste vérifié par rapport à eux. Cela fait gagner du temps et évite les erreurs dues à deux paquets dont les déclarations se contredisent, c'est pourquoi tsc --init l'active. Le prix à payer : une erreur dans vos propres fichiers .d.ts n'est pas signalée non plus.

Questions fréquentes

Qu'est-ce qu'un fichier .d.ts en TypeScript ?

Un fichier de déclaration : il ne contient que des types (signatures de fonctions, interfaces, formes de classes) et aucune implémentation. Il décrit du JavaScript qui existe ailleurs, comme une bibliothèque compilée ou les API intégrées du navigateur, pour que TypeScript puisse vérifier le code qui l'utilise. tsc n'émet jamais de JavaScript pour lui.

Que fait le mot clé declare en TypeScript ?

Il indique au compilateur qu'une valeur existe à l'exécution, sans la créer. declare const VERSION: string; vous permet d'utiliser VERSION comme une chaîne, et la ligne disparaît de la sortie. Si rien ne définit réellement VERSION, le programme échoue à l'exécution avec une ReferenceError.

Comment corriger « Could not find a declaration file for module » ?

L'erreur TS7016 signifie que le paquet n'a pas de types. Installez-les s'ils existent (npm i -D @types/package-name), ou ajoutez un fichier .d.ts avec declare module "package-name" { ... } qui décrit ce que vous utilisez. declare module "package-name"; seul fait taire l'erreur en typant tout le module en any.

Comment générer des fichiers .d.ts à partir de TypeScript ?

Mettez "declaration": true dans tsconfig.json (ou passez --declaration). Chaque fichier .ts produit alors un .d.ts à côté de son .js. Ajoutez emitDeclarationOnly quand un autre outil construit le JavaScript, et declarationMap pour que les éditeurs puissent sauter des types vers votre source.

Quelle est la différence entre declare global et declare module ?

declare global { ... } ajoute des éléments à la portée globale, par exemple une nouvelle propriété sur Array ou une variable globale. declare module "name" { ... } décrit un module que vous importez sous ce nom. declare global doit se trouver dans un module (un fichier avec un import ou un export) ; declare module déclare un nouveau module dans un fichier sans import ni export, et augmente un module existant à l'intérieur d'un module.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER