11 - Déclaration files (.d.ts) et packages types
Ce que tu vas apprendre
- Ce que sont les fichiers
.d.tset 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 :
- Tu publies un package npm et tes consommateurs ont besoin des types
- Tu utilises une lib JavaScript qui n'a pas de types
- 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.tsdecrivent la forme d'un module sans implementation tsc --declarationles généré automatiquement depuis tes fichiers.ts- Le champ
typesdanspackage.json(et dansexports) 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 attwvé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
- TypeScript Handbook - Déclaration Files par Microsoft
- DefinitelyTyped par la communauté TypeScript
- Are The Types Wrong? par Andrew Branch