Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
Declaration files (.d.ts) et DefinitelyTyped
Dans cette leçon6
Imagine qu'à Jurassic Park, on ait découvert d'anciennes tablettes décrivant des espèces de dinosaures, écrites dans une langue inconnue. Les scientifiques ont dû créer des dictionnaires pour comprendre ce que décrivaient ces tablettes. Dans le monde de TypeScript, ce rôle de dictionnaire revient aux fichiers de déclaration (.d.ts) : ils décrivent la forme d'un code JavaScript qui ne contient lui-même aucune information de type. Sans eux, une bibliothèque JavaScript est de type any pour le compilateur (et en mode strict, l'import se solde par une erreur), donc tu ne découvriras une faute de frappe dans le nom d'une méthode qu'une fois l'application lancée.
Que sont les fichiers .d.ts ?
Les fichiers de déclaration (declaration files) sont des fichiers portant l'extension .d.ts, qui contiennent uniquement des informations de type, sans implémentation. Ils décrivent la forme des bibliothèques JavaScript, des modules externes et des objets globaux, et le compilateur ne génère aucun code à partir d'eux. Voici la description d'une bibliothèque de suivi des dinosaures, dans laquelle declare module 'nom' indique à quoi ressemble le module importé sous ce nom :
1// fichier : dinosaur-tracker.d.ts
2// On décrit une bibliothèque JavaScript externe
3
4declare module 'dinosaur-tracker' {
5 export interface DinosaurPosition {
6 id: string;
7 species: string;
8 latitude: number;
9 longitude: number;
10 lastSeen: Date;
11 }
12
13 export function trackDinosaur(id: string): DinosaurPosition;
14 export function getAllPositions(): DinosaurPosition[];
15 export function setAlert(species: string, radius: number): void;
16}Grâce à ce fichier, TypeScript sait quelles fonctions et quels types le module dinosaur-tracker exporte, même si le module lui-même est écrit en JavaScript pur. L'implémentation n'a pas changé d'un octet, seule la description s'est ajoutée. Quand tu écris toi-même une bibliothèque en TypeScript, le compilateur génère ces fichiers pour toi avec l'option declaration: true.
Le mot-clé declare
Le mot declare dit à TypeScript : cet élément existe au runtime, mais tu n'as pas besoin de le compiler, fais-moi simplement confiance. Une déclaration ne crée aucun code, donc si tu mens, le compilateur ne s'en apercevra pas. On l'utilise pour décrire des variables, des fonctions, des classes et des espaces de noms.
Variables globales
Un script chargé par une balise <script> peut définir une configuration globale du parc. On décrit sa forme, pas sa valeur :
1// Variable globale disponible dans le navigateur
2declare const PARK_CONFIG: {
3 name: string;
4 maxCapacity: number;
5 securityLevel: 'low' | 'medium' | 'high' | 'critical';
6};
7
8// Maintenant, on peut l'utiliser avec une type safety complète
9console.log(PARK_CONFIG.name);
10console.log(PARK_CONFIG.securityLevel);
11// PARK_CONFIG.unknownField; // Erreur de compilation !Désormais, faire référence à un champ inexistant provoque une erreur de compilation, même si l'objet lui-même est créé par un tout autre script.
Fonctions globales
Tu décris de la même façon les fonctions d'un script externe, en donnant seulement leur signature, sans le corps :
1// Fonction définie dans un script externe
2declare function initializeFence(
3 zone: string,
4 voltage: number
5): { active: boolean; zone: string };
6
7declare function emergencyShutdown(): void;
8
9// Utilisation avec type safety
10const fence = initializeFence('raptor-paddock', 10000);
11console.log(fence.active); // OK
12// console.log(fence.power); // Erreur ! Cette propriété n'existe pasLa déclaration dit ce que la fonction accepte et renvoie, tandis que son code vit ailleurs.
Classes et namespaces
On peut aussi déclarer des classes entières ainsi que des espaces de noms (namespace), qui regroupent des types et des fonctions liés, comme dans les anciennes bibliothèques chargées globalement :
1declare class SecuritySystem {
2 constructor(zones: string[]);
3 arm(zone: string): void;
4 disarm(zone: string): void;
5 getStatus(): Record<string, boolean>;
6}
7
8declare namespace ParkAPI {
9 interface Visitor {
10 id: string;
11 name: string;
12 ticket: 'standard' | 'vip' | 'researcher';
13 }
14
15 function registerVisitor(name: string, ticket: Visitor['ticket']): Visitor;
16 function getVisitorCount(): number;
17}
18
19// Utilisation
20const system = new SecuritySystem(['zone-a', 'zone-b']);
21system.arm('zone-a');
22
23const visitor = ParkAPI.registerVisitor('Alan Grant', 'researcher');
24console.log(visitor.ticket);À l'intérieur de declare namespace, tu n'as pas besoin d'écrire export, car dans un contexte ambient, tous les membres sont accessibles depuis l'extérieur.
Écrire ses propres fichiers de déclaration
Quand une bibliothèque JavaScript n'a pas de types, tu écris ton propre fichier .d.ts. Celui-ci décrit une ancienne base de données de dinosaures. Omit<DinosaurRecord, 'id'> est un enregistrement sans le champ id, que la base attribuera elle-même, et Partial<DinosaurRecord> rend tous les champs optionnels :
1// fichier : types/legacy-dino-db.d.ts
2// On décrit une ancienne bibliothèque JS de gestion d'une base de dinosaures
3
4declare module 'legacy-dino-db' {
5 export interface DinosaurRecord {
6 id: string;
7 species: string;
8 diet: 'herbivore' | 'carnivore' | 'omnivore';
9 weight: number;
10 height: number;
11 dangerLevel: 1 | 2 | 3 | 4 | 5;
12 }
13
14 export interface QueryOptions {
15 limit?: number;
16 offset?: number;
17 sortBy?: keyof DinosaurRecord;
18 order?: 'asc' | 'desc';
19 }
20
21 export class DinoDB {
22 constructor(connectionString: string);
23 connect(): Promise<void>;
24 disconnect(): Promise<void>;
25 findAll(options?: QueryOptions): Promise<DinosaurRecord[]>;
26 findById(id: string): Promise<DinosaurRecord | null>;
27 insert(record: Omit<DinosaurRecord, 'id'>): Promise<DinosaurRecord>;
28 update(id: string, data: Partial<DinosaurRecord>): Promise<DinosaurRecord>;
29 delete(id: string): Promise<boolean>;
30 }
31
32 // Export par défaut
33 export default DinoDB;
34}Les types utilitaires (utility types) s'assemblent comme des briques : tu pars d'un type de base, tu choisis les champs avec Pick ou Omit, tu les modifies avec Partial ou Required, et pour finir tu verrouilles le tout avec Readonly. Les données d'une mise à jour seraient donc décrites par le type type UpdateDino = Partial<DinosaurRecord>.
DefinitelyTyped et les paquets @types
DefinitelyTyped est un immense dépôt GitHub de fichiers de déclaration pour des milliers de bibliothèques JavaScript, maintenu par la communauté. Chaque ensemble est publié sur npm sous le nom @types/nom-de-la-bibliotheque, donc au lieu d'écrire tes propres fichiers .d.ts, tu installes des types prêts à l'emploi :
1// Installation des types pour des bibliothèques populaires :
2// npm install --save-dev @types/express
3// npm install --save-dev @types/lodash
4// npm install --save-dev @types/node
5
6// Après l'installation de @types/express, tu peux écrire :
7import express, { Request, Response, NextFunction } from 'express';
8
9const app = express();
10
11// TypeScript connaît les types Request, Response, NextFunction
12app.get('/dinosaurs/:id', (req: Request, res: Response) => {
13 const dinoId: string = req.params.id;
14 res.json({ id: dinoId, species: 'T-Rex' });
15});
16
17// Sans @types/express - aucune information de type !
18// req, res seraient de type "any"Tu installes les paquets @types comme dépendances de développement (--save-dev), car les types ne servent que pendant la compilation.
Comment TypeScript trouve-t-il les fichiers de déclaration ?
Quand tu importes un module, TypeScript cherche les types à plusieurs endroits, à peu près dans cet ordre :
1// 1. Types intégrés au paquet (champ "types" dans package.json)
2// Beaucoup de bibliothèques modernes intègrent leurs .d.ts
3// par exemple axios, zod, prisma
4
5// 2. Paquets @types depuis node_modules/@types/
6// Reconnus automatiquement par TypeScript
7// par exemple @types/react, @types/node
8
9// 3. Tes propres fichiers .d.ts dans le projet
10// Configuration dans tsconfig.json :
11// {
12// "compilerOptions": {
13// "typeRoots": ["./node_modules/@types", "./types"],
14// "types": ["node", "express"]
15// },
16// "include": ["src/**/*", "types/**/*"]
17// }
18
19// 4. Triple-slash directives (rarement utilisées)
20/// <reference types="node" />
21/// <reference path="./custom-types.d.ts" />Avant d'installer @types, vérifie que la bibliothèque n'a pas déjà ses propres types. De plus en plus de paquets, comme axios ou zod, les embarquent directement, et un paquet séparé devient alors inutile.
Ambient module declarations
Parfois, tu dois déclarer un module qui n'est pas un paquet npm, par exemple un fichier CSS, une image ou un JSON. L'astérisque du motif '*.css' correspond à n'importe quel chemin qui porte cette extension :
1// fichier : types/assets.d.ts
2
3// Import de fichiers CSS
4declare module '*.css' {
5 const classes: Record<string, string>;
6 export default classes;
7}
8
9// Import de fichiers d'images
10declare module '*.png' {
11 const src: string;
12 export default src;
13}
14
15declare module '*.svg' {
16 const content: string;
17 export default content;
18}
19
20// Import de fichiers JSON
21declare module '*.json' {
22 const value: Record<string, unknown>;
23 export default value;
24}
25
26// Maintenant, tu peux importer ces fichiers avec type safety :
27// import styles from './styles.css';
28// import logo from './logo.png';
29// import dinoData from './dinosaurs.json';La déclaration dit seulement ce que renverra l'import, et c'est le bundler qui se charge de lire le fichier. Pour le JSON, tu peux à la place activer l'option resolveJsonModule, qui fournit le type exact du contenu.
Étendre des types existants (Declaration Merging dans les .d.ts)
Les fichiers .d.ts permettent aussi d'étendre les types de bibliothèques externes. On ajoute à la requête Express des champs que remplit notre middleware :
1// fichier : types/express-extension.d.ts
2// On étend les types d'Express avec des champs supplémentaires
3
4import 'express';
5
6// Le type de req dans les handlers vient du paquet express-serve-static-core
7declare module 'express-serve-static-core' {
8 interface Request {
9 userId?: string;
10 parkZone?: string;
11 securityClearance?: 'visitor' | 'staff' | 'admin';
12 }
13}
14
15// Maintenant, dans le code de l'application :
16// app.use((req, res, next) => {
17// req.userId = 'USR-001'; // OK - TypeScript connaît ce champ
18// req.parkZone = 'zone-a'; // OK
19// req.securityClearance = 'staff'; // OK
20// next();
21// });L'interface Request fusionne avec l'original (declaration merging) au lieu de le remplacer. On étend le module express-serve-static-core, car c'est de là que vient le type de req dans les handlers, et import 'express' fait du fichier un module, condition indispensable pour étendre un module.
Les fichiers de déclaration (.d.ts) et l'écosystème DefinitelyTyped sont les fondations du travail avec TypeScript dans le monde réel : grâce à eux, tu utilises des milliers de bibliothèques JavaScript avec une type safety complète. Mon ordre de préférence : d'abord les types intégrés au paquet, ensuite @types, et ton propre fichier .d.ts en dernier recours, en commençant par les fonctions que tu utilises vraiment. Dans l'éditeur ci-dessous, tu t'entraîneras aux déclarations, et dans la leçon suivante, tu découvriras les template literal types.
Retiens : un fichier .d.ts est le dictionnaire d'une tablette ancienne, il n'y change pas un seul signe, il permet seulement de la lire en toute sécurité.
Code de cette leçon : index.ts
1// Fichiers de déclaration (.d.ts) et DefinitelyTyped
2// Jurassic Park - Système de déclaration de types
3
4// 1. Simulation : déclarer une bibliothèque JS externe
5// Dans un vrai projet, cela serait dans un fichier .d.ts
6
7// declare module 'dinosaur-tracker' {
8// export function trackDinosaur(id: string): DinosaurPosition;
9// }
10
11// 2. Déclarer des variables globales
12// declare const PARK_CONFIG: { name: string; securityLevel: string };
13
14// 3. Simulation de l'usage de types depuis DefinitelyTyped
15// Imaginons qu'on ait installé @types/express
16
17interface DinosaurRecord {
18 id: string;
19 species: string;
20 diet: 'herbivore' | 'carnivore' | 'omnivore';
21 weight: number;
22 dangerLevel: 1 | 2 | 3 | 4 | 5;
23}
24
25interface QueryOptions {
26 limit?: number;
27 offset?: number;
28 sortBy?: keyof DinosaurRecord;
29 order?: 'asc' | 'desc';
30}
31
32// Simulation d'une classe issue d'une bibliothèque externe
33class DinoDB {
34 private records: DinosaurRecord[] = [
35 { id: 'D001', species: 'T-Rex', diet: 'carnivore', weight: 7000, dangerLevel: 5 },
36 { id: 'D002', species: 'Triceratops', diet: 'herbivore', weight: 6000, dangerLevel: 2 },
37 { id: 'D003', species: 'Velociraptor', diet: 'carnivore', weight: 80, dangerLevel: 4 },
38 { id: 'D004', species: 'Brachiosaurus', diet: 'herbivore', weight: 56000, dangerLevel: 1 },
39 ];
40
41 findAll(options?: QueryOptions): DinosaurRecord[] {
42 let result = [...this.records];
43 if (options?.sortBy) {
44 result.sort((a, b) => {
45 const val = String(a[options.sortBy!]).localeCompare(String(b[options.sortBy!]));
46 return options.order === 'desc' ? -val : val;
47 });
48 }
49 const offset = options?.offset || 0;
50 const limit = options?.limit || result.length;
51 return result.slice(offset, offset + limit);
52 }
53
54 findById(id: string): DinosaurRecord | null {
55 return this.records.find(r => r.id === id) || null;
56 }
57}
58
59// 4. Étendre des types (Declaration Merging)
60// Dans un vrai projet on étend par ex. Express.Request
61
62interface ParkRequest {
63 userId?: string;
64 parkZone?: string;
65 securityClearance?: 'visitor' | 'staff' | 'admin';
66}
67
68// Test
69const db = new DinoDB();
70
71console.log("=== Base de dinosaures (simulation @types) ===");
72console.log("\nTous les enregistrements :");
73db.findAll().forEach(d =>
74 console.log(` ${d.species} (régime : ${d.diet}, danger : ${d.dangerLevel})`)
75);
76
77console.log("\nTriés par poids (desc), limite 2 :");
78db.findAll({ sortBy: 'weight', order: 'desc', limit: 2 }).forEach(d =>
79 console.log(` ${d.species} : ${d.weight}kg`)
80);
81
82console.log("\nJe cherche le T-Rex :");
83const trex = db.findById('D001');
84if (trex) {
85 console.log(` Trouvé : ${trex.species}, niveau de danger : ${trex.dangerLevel}`);
86}
87
88// 5. Déclarations ambient - types pour les ressources
89// declare module '*.css' { const classes: Record<string, string>; export default classes; }
90// declare module '*.png' { const src: string; export default src; }
91
92console.log("\n=== Types de déclaration (.d.ts) ===");
93console.log("• declare module - déclaration d'un module externe");
94console.log("• declare const/function - variables/fonctions globales");
95console.log("• @types/xxx - types tout prêts de DefinitelyTyped");
96console.log("• *.d.ts - fichiers de types uniquement, sans implémentation");Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que contiennent les fichiers .d.ts en TypeScript ?
2. Qu'est-ce que le dépôt DefinitelyTyped et les paquets @types ?
Tâches pratiques dans le jeu
- Éditeur de code
Écris des déclarations de types pour une bibliothèque externe et des variables globales avec declare.
- Clique dans l'ordre
Ordonne les éléments d'une déclaration de module externe en TypeScript :
- Éditeur de code
Crée les déclarations de types pour le module dinosaur-tracker avec declare module.
- Tri horizontal
Ordonne dans le bon ordre les éléments d'une déclaration de type Partial :
- Tri vertical
Ordonne les étapes de construction d'un utility type complexe :