TypeScript en pratique - 11 - Déclaration files (.d.ts) et packages types

Comprendre les fichiers .d.ts, générer des déclarations pour tes packages, typer une lib JS externe et publier un package npm avec des types.

11 - Déclaration files (.d.ts) et packages types

Ce que tu vas apprendre

  • Ce que sont les fichiers .d.ts et quand en écrire
  • Comment générer des déclarations avec tsc --declaration
  • Comment typer une lib JavaScript que tu ne contrôles pas
  • Comment publier un package npm avec des types
  • Le rôle de @types/ et DefinitelyTyped
  • Les déclaration maps et leur utilité

Prerequisites

Avoir lu l'article sur les generics contraints dans les libs.


Qu'est-ce qu'un fichier .d.ts

Un fichier .d.ts (déclaration file) decrit la forme d'un module sans contenir d'implementation. C'est comme un fichier header en C : il dit au compilateur quels types existent, quelles fonctions sont exportees, quels paramètres elles prennent.

typescript// utils.d.ts
export declare function formatDate(date: Date, locale?: string): string
export declare function parseDate(input: string): Date | null
export declare const VERSION: string

Le mot-clé declare indique que l'implementation est ailleurs. Le compilateur TypeScript utilise ces fichiers pour vérifier les types sans avoir besoin du code source.

Quand tu n'as pas besoin d'écrire de .d.ts

Si ton projet est 100% TypeScript, tu n'as pas besoin d'écrire des .d.ts a la main. Le compilateur comprend directement tes fichiers .ts.

Les .d.ts sont utiles dans trois cas :

  1. Tu publies un package npm et tes consommateurs ont besoin des types
  2. Tu utilises une lib JavaScript qui n'a pas de types
  3. Tu veux déclarer des types globaux (variables d'environnement, modules CSS, etc.)

Generer les déclarations avec tsc

Pour un package que tu publies, configure le tsconfig pour générer les déclarations :

jsonc// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist/types",
    "outDir": "./dist",
    "emitDeclarationOnly": false
  },
  "include": ["src/**/*.ts"]
}

Avec declaration: true, tsc généré un .d.ts pour chaque fichier .ts :

src/
  index.ts
  utils.ts

dist/
  index.js
  utils.js
  types/
    index.d.ts
    utils.d.ts

Si tu utilises un bundler (esbuild, rollup) pour le JS et que tu veux seulement les types, utilise emitDeclarationOnly: true :

jsonc{
  "compilerOptions": {
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "./dist"
  }
}

Le bundler généré le JS, tsc généré les .d.ts.

Le champ types dans package.json

Pour que les consommateurs de ton package trouvent les types, ajoute le champ types (ou typings) dans package.json :

json{
  "name": "my-utils",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"]
}

Avec le champ exports (Node 16+), tu peux etre plus precis :

json{
  "name": "my-utils",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}

L'entree "types" doit etre en premier dans chaque condition. TypeScript la lit avant les autres.

Typer une lib JavaScript externe

Quand tu utilises une lib qui n'a pas de types, tu as trois options.

Option 1 : @types/ sur npm

Beaucoup de libs ont des types communautaires dans le scope @types/. Par exemple :

bashpnpm add -D @types/lodash
pnpm add -D @types/express
pnpm add -D @types/node

Ces packages viennent du dépôt DefinitelyTyped, un monorepo geant avec des déclarations pour des milliers de libs.

Option 2 : déclaration locale

Si la lib n'a pas de @types/, créé un fichier de déclaration dans ton projet :

typescript// types/old-lib.d.ts
declare module "old-lib" {
  export function doSomething(input: string): number
  export function doSomethingElse(options: {
    verbose?: boolean
    timeout?: number
  }): Promise<void>

  export interface OldLibConfig {
    apiKey: string
    baseUrl: string
  }

  export default function init(config: OldLibConfig): void
}

Pour que TypeScript trouve ce fichier, ajoute le dossier types/ dans ton tsconfig :

jsonc{
  "compilerOptions": {
    "typeRoots": ["./types", "./node_modules/@types"]
  }
}

Option 3 : déclaration minimale

Si tu veux juste que TypeScript arrêté de se plaindre sans typer precisement :

typescript// types/old-lib.d.ts
declare module "old-lib"

Ca déclaré le module avec le type any. C'est un pansement, pas une solution. Mais c'est utile pendant une migration progressive.

Déclarations globales

Pour déclarer des types globaux (variables d'environnement, objets sur window, modules CSS) :

typescript// types/global.d.ts

// Modules CSS
declare module "*.module.css" {
  const classes: Record<string, string>
  export default classes
}

// Modules SVG
declare module "*.svg" {
  const content: string
  export default content
}

// Variables globales
declare global {
  interface Window {
    __APP_CONFIG__: {
      apiUrl: string
      version: string
    }
  }
}

export {} // necessaire pour que le fichier soit traite comme un module

Le export {} a la fin est un détail qui m'a fait perdre du temps la première fois. Sans lui, TypeScript traite le fichier comme un script (pas un module) et le declare global ne fonctionne pas comme attendu.

Déclaration maps

Les déclaration maps (declarationMap: true) generent des fichiers .d.ts.map qui lient les déclarations au code source original :

jsonc{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true
  }
}

Avec les déclaration maps, quand un utilisateur de ta lib fait "Go to Définition" dans son IDE, il arrive dans ton fichier .ts source au lieu du .d.ts généré. C'est un détail d'experience développeur qui fait une vraie différence.

Sur paltemps.fr, les packages internes du monorepo utilisent tous les déclaration maps. Ca permet de naviguer dans le code comme si c'etait un seul projet.

Verifier tes déclarations

Avant de publier, vérifié que tes types sont corrects :

bash# Generer les declarations
tsc --declaration --emitDeclarationOnly

# Verifier qu'un projet consommateur peut les utiliser
cd /tmp && mkdir test-types && cd test-types
npm init -y
npm install ../mon-package

Cree un fichier de test :

typescript// test.ts
import { formatDate, parseDate } from "mon-package"

const result = formatDate(new Date())  // devrait compiler
const bad = formatDate(42)             // devrait echouer

Tu peux aussi utiliser attw (Are The Types Wrong) pour vérifier automatiquement :

bashnpx @arethetypeswrong/cli --pack .

Cet outil détecté les problèmes courants : types manquants, résolution ESM/CJS incorrecte, exports mal configures.

Publier un package type avec les types

Voici la checklist minimale :

json{
  "name": "@mon-scope/utils",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "prepublishOnly": "npm run build"
  }
}

tsup généré le JS (CJS + ESM) et les .d.ts en une seule commande. C'est plus simple que de configurer tsc + un bundler séparément.

DefinitelyTyped : contribuer

Si tu ecris des types pour une lib populaire, tu peux les contribuer a DefinitelyTyped :

bashgit clone https://github.com/DefinitelyTyped/DefinitelyTyped
cd DefinitelyTyped
npx dts-gen --dt --name ma-lib --template module

Cela généré un squelette dans types/ma-lib/. Tu remplis les types, tu ajoutes des tests, et tu fais une PR. Le processus est bien documente mais la review peut prendre quelques jours.


Résumé

  • Les fichiers .d.ts decrivent la forme d'un module sans implementation
  • tsc --declaration les généré automatiquement depuis tes fichiers .ts
  • Le champ types dans package.json (et dans exports) indique ou trouver les déclarations
  • @types/ et DefinitelyTyped fournissent des types pour les libs JavaScript sans types natifs
  • Les déclaration maps (declarationMap: true) ameliorent le "Go to Définition" pour les consommateurs
  • attw vérifié que tes types sont correctement configures avant publication

Article précédent : 10 - Generics contraints dans les libs

Article suivant : 12 - Monorepo et partage de types

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.