Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
Module Augmentation et Declaration Merging
Dans cette leçon3
Imagine que le parc a acheté un système de caméras clé en main, et que tu veux ajouter à ses rapports un champ dinosaurId. Tu ne peux pas modifier le code du fournisseur, car tes changements disparaîtraient à la prochaine mise à jour. Le module augmentation et le declaration merging sont des fonctionnalités avancées de TypeScript qui permettent d'étendre des modules existants et de fusionner des déclarations sans toucher à l'original. Ces mécanismes sont essentiels quand on travaille avec des bibliothèques externes, qu'on crée des plugins ou qu'on étend des frameworks.
Declaration Merging
Le declaration merging (fusion des déclarations) signifie que le compilateur colle plusieurs déclarations portant le même nom en une seule définition. Cela concerne les interfaces, les espaces de noms (namespace) ainsi que les paires classe et namespace, fonction et namespace, et enum et namespace. En revanche, les alias de types (type) et deux classes du même nom ne fusionnent pas.
Les bases du declaration merging
Le cas le plus fréquent, ce sont deux interfaces du même nom. À côté, tu vois la fusion d'espaces de noms ainsi que celle d'une classe avec un namespace, qui ajoute à la classe des types et des fonctions statiques :
1// Interface merging - le cas le plus fréquent
2interface User {
3 id: number;
4 name: string;
5}
6
7interface User {
8 email: string; // On ajoute une nouvelle propriété
9 age?: number; // Et une autre, optionnelle
10}
11
12// TypeScript fusionne automatiquement les deux déclarations
13const user: User = {
14 id: 1,
15 name: "Jean Dupont",
16 email: "jean@example.com"
17 // age est optionnel
18};
19
20// Namespace merging
21namespace MyApp {
22 export interface Config {
23 apiUrl: string;
24 }
25
26 export function initialize(config: Config): void {
27 console.log("Initialisation avec :", config.apiUrl);
28 }
29}
30
31namespace MyApp {
32 export interface Config {
33 timeout?: number; // On étend l'interface Config
34 }
35
36 export class Logger {
37 log(message: string): void {
38 console.log(`[MyApp]: ${message}`);
39 }
40 }
41}
42
43// On utilise les déclarations fusionnées
44const config: MyApp.Config = {
45 apiUrl: "https://api.example.com",
46 timeout: 5000
47};
48
49const logger = new MyApp.Logger();
50logger.log("Application démarrée");
51
52// Class and namespace merging
53class Album {
54 constructor(public title: string, public artist: string) {}
55}
56
57namespace Album {
58 export interface Track {
59 number: number;
60 title: string;
61 duration: number;
62 }
63
64 export function createFromJSON(json: string): Album {
65 const data = JSON.parse(json);
66 return new Album(data.title, data.artist);
67 }
68}
69
70// On utilise la classe et le namespace ensemble
71const album = new Album("The Wall", "Pink Floyd");
72const track: Album.Track = {
73 number: 1,
74 title: "In The Flesh?",
75 duration: 196
76};
77
78const albumFromJSON = Album.createFromJSON('{"title": "Dark Side", "artist": "Pink Floyd"}');L'objet user doit avoir les champs des deux déclarations, et Album fonctionne à la fois comme classe (new Album(...)) et comme espace de noms (Album.Track). Si les deux interfaces déclarent le même champ, ses types doivent être identiques, sinon le compilateur signale une erreur.
Fusion de fonctions et de namespaces
Une fonction fusionnée avec un namespace est un objet qu'on peut appeler et qui possède des champs supplémentaires, comme dans de nombreuses bibliothèques JavaScript. Le même principe fonctionne avec un enum :
1// Function and namespace merging - pratique pour créer des callable objects
2function createCounter(initial: number = 0): () => number {
3 let count = initial;
4 return () => ++count;
5}
6
7namespace createCounter {
8 export interface Options {
9 initial?: number;
10 step?: number;
11 max?: number;
12 }
13
14 export function withOptions(options: Options): () => number {
15 let count = options.initial || 0;
16 const step = options.step || 1;
17 const max = options.max || Infinity;
18
19 return () => {
20 if (count + step > max) {
21 return count;
22 }
23 count += step;
24 return count;
25 };
26 }
27
28 export function reset(counter: () => number): void {
29 // Implémentation de la remise à zéro
30 console.log("Counter reset");
31 }
32}
33
34// On utilise la fonction et son namespace
35const counter1 = createCounter(10);
36const counter2 = createCounter.withOptions({ initial: 0, step: 5, max: 100 });
37
38console.log(counter1()); // 11
39console.log(counter2()); // 5
40createCounter.reset(counter1);
41
42// Enum and namespace merging
43enum Status {
44 Idle = "IDLE",
45 Loading = "LOADING",
46 Success = "SUCCESS",
47 Error = "ERROR"
48}
49
50namespace Status {
51 export function isComplete(status: Status): boolean {
52 return status === Status.Success || status === Status.Error;
53 }
54
55 export function isInProgress(status: Status): boolean {
56 return status === Status.Loading;
57 }
58
59 export interface Metadata {
60 timestamp: Date;
61 message?: string;
62 }
63}
64
65// On utilise l'enum avec des fonctions supplémentaires
66const currentStatus = Status.Loading;
67console.log(Status.isInProgress(currentStatus)); // true
68console.log(Status.isComplete(currentStatus)); // falsecreateCounter(10) renvoie un compteur qui démarre à 10, et createCounter.withOptions est une fonction rattachée au même nom. L'enum Status a reçu des méthodes auxiliaires, et ses valeurs n'ont pas changé.
Merging avec les génériques
Les interfaces génériques fusionnées doivent avoir des paramètres de type identiques, noms compris. La classe implémente alors la somme de toutes les déclarations :
1// Generic interface merging
2interface Collection<T> {
3 items: T[];
4 add(item: T): void;
5}
6
7interface Collection<T> {
8 remove(item: T): boolean;
9 find(predicate: (item: T) => boolean): T | undefined;
10}
11
12class ArrayCollection<T> implements Collection<T> {
13 items: T[] = [];
14
15 add(item: T): void {
16 this.items.push(item);
17 }
18
19 remove(item: T): boolean {
20 const index = this.items.indexOf(item);
21 if (index > -1) {
22 this.items.splice(index, 1);
23 return true;
24 }
25 return false;
26 }
27
28 find(predicate: (item: T) => boolean): T | undefined {
29 return this.items.find(predicate);
30 }
31}
32
33// Generic namespace merging
34namespace DataStructures {
35 export interface Node<T> {
36 value: T;
37 next?: Node<T>;
38 }
39
40 export class LinkedList<T> {
41 head?: Node<T>;
42
43 add(value: T): void {
44 const node: Node<T> = { value };
45 if (!this.head) {
46 this.head = node;
47 } else {
48 let current = this.head;
49 while (current.next) {
50 current = current.next;
51 }
52 current.next = node;
53 }
54 }
55 }
56}
57
58namespace DataStructures {
59 export interface Node<T> {
60 prev?: Node<T>; // On ajoute un pointeur vers l'élément précédent
61 }
62
63 export class DoublyLinkedList<T> extends LinkedList<T> {
64 // Implémentation d'une liste doublement chaînée
65 }
66}ArrayCollection doit avoir add, remove et find, car Collection<T> ne forme plus qu'une seule interface. La deuxième partie du namespace DataStructures ajoute au nœud un pointeur prev.
Module Augmentation
Le module augmentation consiste à étendre les types d'un module qui ne t'appartient pas. Tu importes le module d'origine (ce qui fait de ton fichier un module), tu ouvres sa déclaration avec declare module 'nom', tu ajoutes de nouveaux champs ou de nouvelles méthodes à l'interface, et TypeScript fusionne lui-même les déclarations. Pour un fichier local, tu écriras par exemple declare module './dinosaur' { interface Dinosaur { fly(): void } }.
Étendre des modules externes
On ajoute les données de l'utilisateur à la requête Express, et des méthodes auxiliaires à la réponse. On étend express-serve-static-core, car c'est de là que viennent les types de req et res dans les handlers :
1// Extension des types d'Express (req et res dans les handlers viennent d'express-serve-static-core)
2declare module "express-serve-static-core" {
3 interface Request {
4 user?: {
5 id: string;
6 email: string;
7 roles: string[];
8 };
9 requestId?: string;
10 }
11
12 interface Response {
13 sendSuccess(data: any): void;
14 sendError(error: Error, statusCode?: number): void;
15 }
16}
17
18// Implémentation des extensions
19import express from "express";
20
21const app = express();
22
23// Middleware qui ajoute user et requestId
24app.use((req, res, next) => {
25 req.requestId = crypto.randomUUID();
26 // Authentification et ajout de l'utilisateur
27 req.user = {
28 id: "123",
29 email: "user@example.com",
30 roles: ["user", "admin"]
31 };
32 next();
33});
34
35// Extension de Response avec de nouvelles méthodes
36app.use((req, res, next) => {
37 res.sendSuccess = function(data: any) {
38 this.json({
39 success: true,
40 data,
41 requestId: req.requestId
42 });
43 };
44
45 res.sendError = function(error: Error, statusCode: number = 500) {
46 this.status(statusCode).json({
47 success: false,
48 error: error.message,
49 requestId: req.requestId
50 });
51 };
52
53 next();
54});
55
56// On utilise les types étendus
57app.get("/profile", (req, res) => {
58 if (!req.user) {
59 return res.sendError(new Error("Unauthorized"), 401);
60 }
61
62 res.sendSuccess({
63 user: req.user,
64 requestId: req.requestId
65 });
66});L'augmentation ne modifie que les types, donc c'est toujours un middleware qui ajoute l'implémentation de sendSuccess et sendError avant que le handler ne s'en serve. La règle générale : étends le module qui déclare réellement le type, et non celui qui se contente de le réexporter.
Étendre les types globaux
Le bloc declare global étend les types globaux, comme Window, Array ou String. Il ne fonctionne que dans un fichier qui est un module, donc si ton fichier n'a pas d'imports, ajoute export {} à la fin :
1// Extension des objets globaux
2declare global {
3 interface Window {
4 myApp: {
5 version: string;
6 config: {
7 apiUrl: string;
8 debugMode: boolean;
9 };
10 utils: {
11 formatDate(date: Date): string;
12 parseJSON<T>(json: string): T | null;
13 };
14 };
15 }
16
17 interface Array<T> {
18 groupBy<K extends keyof T>(key: K): Record<string, T[]>;
19 chunk(size: number): T[][];
20 }
21
22 interface String {
23 capitalize(): string;
24 toSlug(): string;
25 }
26}
27
28// Implémentation des extensions d'Array
29Array.prototype.groupBy = function<T, K extends keyof T>(this: T[], key: K): Record<string, T[]> {
30 return this.reduce((acc, item) => {
31 const groupKey = String(item[key]);
32 if (!acc[groupKey]) {
33 acc[groupKey] = [];
34 }
35 acc[groupKey].push(item);
36 return acc;
37 }, {} as Record<string, T[]>);
38};
39
40Array.prototype.chunk = function<T>(this: T[], size: number): T[][] {
41 const chunks: T[][] = [];
42 for (let i = 0; i < this.length; i += size) {
43 chunks.push(this.slice(i, i + size));
44 }
45 return chunks;
46};
47
48// Implémentation des extensions de String
49String.prototype.capitalize = function(this: string): string {
50 return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
51};
52
53String.prototype.toSlug = function(this: string): string {
54 return this
55 .toLowerCase()
56 .trim()
57 .replace(/[^\w\s-]/g, '')
58 .replace(/[\s_-]+/g, '-')
59 .replace(/^-+|-+$/g, '');
60};
61
62// On utilise les extensions
63const users = [
64 { name: "Jan", age: 30, department: "IT" },
65 { name: "Anna", age: 25, department: "HR" },
66 { name: "Piotr", age: 35, department: "IT" }
67];
68
69const grouped = users.groupBy("department");
70console.log(grouped); // { IT: [...], HR: [...] }
71
72const chunks = [1, 2, 3, 4, 5, 6, 7, 8].chunk(3);
73console.log(chunks); // [[1, 2, 3], [4, 5, 6], [7, 8]]
74
75const title = "hello world from typescript".capitalize();
76console.log(title); // "Hello world from typescript"
77
78const slug = "Hello World! From TypeScript".toSlug();
79console.log(slug); // "hello-world-from-typescript"
80
81// Extensions de Window
82window.myApp = {
83 version: "1.0.0",
84 config: {
85 apiUrl: "https://api.example.com",
86 debugMode: true
87 },
88 utils: {
89 formatDate(date: Date): string {
90 return date.toLocaleDateString("fr-FR");
91 },
92 parseJSON<T>(json: string): T | null {
93 try {
94 return JSON.parse(json) as T;
95 } catch {
96 return null;
97 }
98 }
99 }
100};Techniquement, cela fonctionne, mais je ne te recommande pas d'étendre les prototypes intégrés. Ce sont justement ce genre de rustines (la bibliothèque Sugar ajoutait son propre groupBy aux tableaux) qui ont empêché le standard d'utiliser le nom Array.prototype.groupBy, si bien qu'ES2024 a introduit la méthode statique Object.groupBy. Il est plus sûr d'écrire des fonctions auxiliaires ordinaires ou d'étendre tes propres objets, comme window.myApp.
Un système de plugins avec le module augmentation
Un système de plugins permet à différentes parties de l'application d'ajouter leurs propres types de contexte et de hooks à un module commun :
1// Système de plugins de base
2interface PluginSystem {
3 plugins: Map<string, Plugin>;
4 register(plugin: Plugin): void;
5 execute(hookName: string, context: any): void;
6}
7
8interface Plugin {
9 name: string;
10 version: string;
11 hooks?: {
12 [hookName: string]: (context: any) => void | Promise<void>;
13 };
14}
15
16class PluginManager implements PluginSystem {
17 plugins = new Map<string, Plugin>();
18
19 register(plugin: Plugin): void {
20 this.plugins.set(plugin.name, plugin);
21 console.log(`Plugin ${plugin.name} v${plugin.version} enregistré`);
22 }
23
24 async execute(hookName: string, context: any): Promise<void> {
25 for (const [name, plugin] of this.plugins) {
26 if (plugin.hooks && plugin.hooks[hookName]) {
27 await plugin.hooks[hookName](context);
28 }
29 }
30 }
31}
32
33// Extension du système de plugins par différents modules
34declare module "./plugin-system" {
35 interface PluginContext {
36 user?: {
37 id: string;
38 name: string;
39 };
40 request?: {
41 url: string;
42 method: string;
43 };
44 }
45
46 interface PluginHooks {
47 beforeRequest?: (context: PluginContext) => void;
48 afterRequest?: (context: PluginContext & { response: any }) => void;
49 onError?: (context: PluginContext & { error: Error }) => void;
50 }
51}
52
53// Plugin d'authentification
54const authPlugin: Plugin = {
55 name: "auth",
56 version: "1.0.0",
57 hooks: {
58 beforeRequest(context) {
59 console.log("Vérification de l'authentification...");
60 if (!context.user) {
61 throw new Error("Non authentifié");
62 }
63 }
64 }
65};
66
67// Plugin de journalisation
68const loggingPlugin: Plugin = {
69 name: "logging",
70 version: "1.0.0",
71 hooks: {
72 beforeRequest(context) {
73 console.log(`[${new Date().toISOString()}] ${context.request?.method} ${context.request?.url}`);
74 },
75 afterRequest(context) {
76 console.log(`Réponse pour ${context.request?.url} :`, context.response);
77 },
78 onError(context) {
79 console.error(`Erreur pour ${context.request?.url} :`, context.error);
80 }
81 }
82};
83
84// Utilisation du système de plugins
85const pluginManager = new PluginManager();
86pluginManager.register(authPlugin);
87pluginManager.register(loggingPlugin);
88
89// Simulation d'une requête
90const context = {
91 user: { id: "123", name: "Jan" },
92 request: { url: "/api/data", method: "GET" }
93};
94
95pluginManager.execute("beforeRequest", context);L'augmentation declare module './plugin-system' ne fonctionne que si ce fichier existe, et le code lui-même doit se trouver dans un module. Dans un fichier sans imports, l'interface Plugin fusionnerait avec l'interface globale Plugin des définitions du DOM, qui décrit les plugins du navigateur. C'est le declaration merging sous sa forme la moins souhaitable.
Applications pratiques
Étendre des bibliothèques d'UI
Material UI encourage ouvertement l'augmentation : tu ajoutes tes propres champs de thème et variantes de typographie aux interfaces Theme, ThemeOptions et TypographyVariants :
1// Extension de Material-UI
2declare module "@mui/material/styles" {
3 interface Theme {
4 custom: {
5 borderRadius: {
6 small: number;
7 medium: number;
8 large: number;
9 };
10 animations: {
11 fast: string;
12 normal: string;
13 slow: string;
14 };
15 };
16 }
17
18 interface ThemeOptions {
19 custom?: {
20 borderRadius?: {
21 small?: number;
22 medium?: number;
23 large?: number;
24 };
25 animations?: {
26 fast?: string;
27 normal?: string;
28 slow?: string;
29 };
30 };
31 }
32
33 interface TypographyVariants {
34 label: React.CSSProperties;
35 caption2: React.CSSProperties;
36 }
37
38 interface TypographyVariantsOptions {
39 label?: React.CSSProperties;
40 caption2?: React.CSSProperties;
41 }
42}
43
44declare module "@mui/material/Typography" {
45 interface TypographyPropsVariantOverrides {
46 label: true;
47 caption2: true;
48 }
49}
50
51// Création d'un thème étendu
52import { createTheme } from "@mui/material/styles";
53
54const theme = createTheme({
55 custom: {
56 borderRadius: {
57 small: 4,
58 medium: 8,
59 large: 16
60 },
61 animations: {
62 fast: "150ms",
63 normal: "300ms",
64 slow: "500ms"
65 }
66 },
67 typography: {
68 label: {
69 fontSize: "0.75rem",
70 fontWeight: 600,
71 textTransform: "uppercase"
72 },
73 caption2: {
74 fontSize: "0.625rem",
75 fontWeight: 400
76 }
77 }
78});
79
80// Utilisation des types étendus
81import { Typography, Box } from "@mui/material";
82
83function CustomComponent() {
84 return (
85 <Box
86 sx={{
87 borderRadius: (theme) => theme.custom.borderRadius.medium,
88 transition: (theme) => `all ${theme.custom.animations.normal} ease`
89 }}
90 >
91 <Typography variant="label">Custom Label</Typography>
92 <Typography variant="caption2">Tiny caption text</Typography>
93 </Box>
94 );
95}Theme décrit le thème final, et ThemeOptions les données passées à createTheme, c'est pourquoi les champs y sont optionnels. Le code contient du JSX, donc dans un projet, tu le places dans un fichier .tsx.
Étendre des frameworks
De la même façon, tu étendras les types de Next.js pour les API routes du Pages Router, en ajoutant la session et des méthodes auxiliaires :
1// Extension de Next.js
2declare module "next" {
3 export interface NextApiRequest {
4 session?: {
5 user: {
6 id: string;
7 email: string;
8 permissions: string[];
9 };
10 expiresAt: Date;
11 };
12
13 // Méthodes auxiliaires
14 validateBody<T>(schema: any): T;
15 requireAuth(): void;
16 hasPermission(permission: string): boolean;
17 }
18
19 export interface NextApiResponse {
20 success<T>(data: T, statusCode?: number): void;
21 error(message: string, statusCode?: number): void;
22 paginated<T>(items: T[], total: number, page: number, pageSize: number): void;
23 }
24}
25
26// Middleware qui étend Next.js
27import { NextApiRequest, NextApiResponse } from "next";
28
29export function withExtensions(
30 handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>
31) {
32 return async (req: NextApiRequest, res: NextApiResponse) => {
33 // Ajouter des méthodes à la requête
34 req.validateBody = function<T>(schema: any): T {
35 // Implémentation de la validation
36 return req.body as T;
37 };
38
39 req.requireAuth = function() {
40 if (!this.session) {
41 throw new Error("Authentication required");
42 }
43 };
44
45 req.hasPermission = function(permission: string): boolean {
46 return this.session?.user.permissions.includes(permission) || false;
47 };
48
49 // Ajouter des méthodes à la réponse
50 res.success = function<T>(data: T, statusCode = 200) {
51 this.status(statusCode).json({
52 success: true,
53 data
54 });
55 };
56
57 res.error = function(message: string, statusCode = 500) {
58 this.status(statusCode).json({
59 success: false,
60 error: message
61 });
62 };
63
64 res.paginated = function<T>(items: T[], total: number, page: number, pageSize: number) {
65 this.json({
66 success: true,
67 data: items,
68 pagination: {
69 total,
70 page,
71 pageSize,
72 totalPages: Math.ceil(total / pageSize)
73 }
74 });
75 };
76
77 // Appeler le handler
78 try {
79 await handler(req, res);
80 } catch (error) {
81 res.error(error instanceof Error ? error.message : "Internal server error");
82 }
83 };
84}
85
86// Utilisation dans une API route
87export default withExtensions(async (req, res) => {
88 req.requireAuth();
89
90 if (!req.hasPermission("read:users")) {
91 return res.error("Insufficient permissions", 403);
92 }
93
94 const data = req.validateBody<{ search?: string }>(/* schema */);
95
96 // Logique métier
97 const users = []; // Récupérer les utilisateurs
98 const total = 100;
99
100 res.paginated(users, total, 1, 20);
101});Dans l'App Router, les routes d'API sont gérées par des Route Handlers basés sur les objets standard Request et Response, donc là-bas, au lieu d'une augmentation, tu écris généralement des fonctions auxiliaires. Pour les types réexportés, vérifie de quel fichier ils proviennent, comme pour Express.
Le module augmentation et le declaration merging sont des outils puissants pour étendre du code existant avec souplesse. La clé est de comprendre quand utiliser chaque technique et comment elles peuvent coopérer dans des systèmes extensibles. Mon conseil : range tes augmentations dans un dossier à part, par exemple types/, pour que toute l'équipe sache où chercher les ajouts aux types des autres. Dans l'éditeur ci-dessous, tu t'exerceras à la fusion des déclarations, et dans la leçon suivante, tu découvriras le Test-Driven Development.
Retiens : une augmentation, c'est un autocollant annoté posé sur la carte du parc d'un autre, qui n'efface rien et ne fait qu'ajouter de nouvelles informations.
Code de cette leçon : index.ts
1// Exemples de module augmentation et declaration merging
2
3// 1. Interface merging - fusion de déclarations
4interface User {
5 id: number;
6 name: string;
7}
8
9interface User {
10 email: string; // Ajoutons de nouvelles propriétés
11 age?: number; // Propriété optionnelle
12}
13
14// TypeScript fusionne automatiquement les deux déclarations
15const user: User = {
16 id: 1,
17 name: "Jan Kowalski",
18 email: "jan@example.com",
19 age: 30
20};
21
22// 2. Namespace merging
23namespace AppConfig {
24 export interface Settings {
25 apiUrl: string;
26 timeout: number;
27 }
28
29 export function getDefaultSettings(): Settings {
30 return {
31 apiUrl: "https://api.example.com",
32 timeout: 5000
33 };
34 }
35}
36
37namespace AppConfig {
38 export interface Settings {
39 debugMode?: boolean; // On étend l'interface
40 }
41
42 export class ConfigManager {
43 private settings: Settings;
44
45 constructor() {
46 this.settings = getDefaultSettings();
47 }
48
49 updateSettings(updates: Partial<Settings>): void {
50 this.settings = { ...this.settings, ...updates };
51 }
52
53 getSettings(): Settings {
54 return this.settings;
55 }
56 }
57}
58
59// On utilise les déclarations fusionnées
60const configManager = new AppConfig.ConfigManager();
61configManager.updateSettings({ debugMode: true });
62console.log("Réglages :", configManager.getSettings());
63
64// 3. Function et namespace merging
65function createLogger(prefix: string) {
66 return {
67 log(message: string) {
68 console.log(`[${prefix}] ${message}`);
69 }
70 };
71}
72
73namespace createLogger {
74 export interface Options {
75 timestamp?: boolean;
76 color?: string;
77 }
78
79 export function withOptions(prefix: string, options: Options) {
80 return {
81 log(message: string) {
82 const timestamp = options.timestamp ? new Date().toISOString() : '';
83 console.log(`${timestamp} [${prefix}] ${message}`);
84 }
85 };
86 }
87
88 export const DEFAULT_OPTIONS: Options = {
89 timestamp: true,
90 color: 'default'
91 };
92}
93
94// On utilise la fonction et le namespace
95const logger1 = createLogger("APP");
96const logger2 = createLogger.withOptions("DEBUG", { timestamp: true });
97
98logger1.log("Message simple");
99logger2.log("Message avec horodatage");
100
101// 4. Étendre les types globaux
102declare global {
103 interface Array<T> {
104 first(): T | undefined;
105 last(): T | undefined;
106 groupBy<K extends keyof T>(key: K): Record<string, T[]>;
107 }
108
109 interface String {
110 capitalize(): string;
111 toKebabCase(): string;
112 }
113}
114
115// Implémentation des extensions
116Array.prototype.first = function<T>(this: T[]): T | undefined {
117 return this[0];
118};
119
120Array.prototype.last = function<T>(this: T[]): T | undefined {
121 return this[this.length - 1];
122};
123
124Array.prototype.groupBy = function<T, K extends keyof T>(
125 this: T[],
126 key: K
127): Record<string, T[]> {
128 return this.reduce((groups, item) => {
129 const groupKey = String(item[key]);
130 groups[groupKey] = groups[groupKey] || [];
131 groups[groupKey].push(item);
132 return groups;
133 }, {} as Record<string, T[]>);
134};
135
136String.prototype.capitalize = function(this: string): string {
137 return this.charAt(0).toUpperCase() + this.slice(1);
138};
139
140String.prototype.toKebabCase = function(this: string): string {
141 return this
142 .replace(/([A-Z])/g, '-$1')
143 .toLowerCase()
144 .replace(/^-/, '');
145};
146
147// Test des extensions
148const numbers = [1, 2, 3, 4, 5];
149console.log("Premier élément :", numbers.first()); // 1
150console.log("Dernier élément :", numbers.last()); // 5
151
152const products = [
153 { name: "Laptop", category: "Electronics", price: 1000 },
154 { name: "Mouse", category: "Electronics", price: 50 },
155 { name: "Desk", category: "Furniture", price: 300 }
156];
157
158const grouped = products.groupBy("category");
159console.log("Produits groupés :", grouped);
160
161const text = "hello world";
162console.log("Capitalize :", text.capitalize()); // "Hello world"
163
164const camelCase = "myVariableName";
165console.log("Kebab case :", camelCase.toKebabCase()); // "my-variable-name"
166
167// 5. Class et namespace merging
168class EventEmitter {
169 private events: Map<string, Function[]> = new Map();
170
171 on(event: string, handler: Function): void {
172 if (!this.events.has(event)) {
173 this.events.set(event, []);
174 }
175 this.events.get(event)!.push(handler);
176 }
177
178 emit(event: string, ...args: any[]): void {
179 const handlers = this.events.get(event);
180 if (handlers) {
181 handlers.forEach(handler => handler(...args));
182 }
183 }
184}
185
186namespace EventEmitter {
187 export interface EventMap {
188 connect: { userId: string; timestamp: Date };
189 disconnect: { userId: string; reason: string };
190 message: { userId: string; text: string };
191 }
192
193 export type EventHandler<K extends keyof EventMap> =
194 (data: EventMap[K]) => void;
195
196 export function createTypedEmitter() {
197 const emitter = new EventEmitter();
198
199 return {
200 on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
201 emitter.on(event, handler);
202 },
203
204 emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
205 emitter.emit(event, data);
206 }
207 };
208 }
209}
210
211// On utilise l'émetteur d'événements typé
212const typedEmitter = EventEmitter.createTypedEmitter();
213
214typedEmitter.on('connect', (data) => {
215 console.log(`L'utilisateur ${data.userId} s'est connecté à ${data.timestamp}`);
216});
217
218typedEmitter.on('message', (data) => {
219 console.log(`${data.userId} : ${data.text}`);
220});
221
222typedEmitter.emit('connect', {
223 userId: 'user123',
224 timestamp: new Date()
225});
226
227typedEmitter.emit('message', {
228 userId: 'user123',
229 text: 'Bienvenue à tous !'
230});
231
232// 6. Module augmentation (simulation)
233// Dans une vraie application, cela serait dans un fichier séparé
234interface MyLibrary {
235 version: string;
236 utils: {
237 formatDate(date: Date): string;
238 };
239}
240
241// On étend la bibliothèque
242interface MyLibrary {
243 plugins: Plugin[];
244 registerPlugin(plugin: Plugin): void;
245}
246
247interface Plugin {
248 name: string;
249 init(): void;
250}
251
252// Implémentation de la bibliothèque étendue
253const myLibrary: MyLibrary = {
254 version: "1.0.0",
255 utils: {
256 formatDate(date: Date): string {
257 return date.toLocaleDateString('fr-FR');
258 }
259 },
260 plugins: [],
261 registerPlugin(plugin: Plugin): void {
262 this.plugins.push(plugin);
263 plugin.init();
264 console.log(`Plugin ${plugin.name} enregistré`);
265 }
266};
267
268// Test
269const datePlugin: Plugin = {
270 name: "DateFormatter",
271 init() {
272 console.log("Plugin DateFormatter initialisé");
273 }
274};
275
276myLibrary.registerPlugin(datePlugin);
277console.log("Version de la bibliothèque :", myLibrary.version);
278console.log("Date formatée :", myLibrary.utils.formatDate(new Date()));
279
280// Export pour éviter les erreurs de déclarations globales
281export {};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. Quelles déclarations peuvent être fusionnées automatiquement en TypeScript ?
Tâches pratiques dans le jeu
- Clique dans l'ordre
Ordonne les étapes de création des ambient modules.
- Éditeur de code
Étends des types existants grâce au declaration merging.
- Clique dans l'ordre
Ordonne les éléments d'une déclaration de module augmentation :
- Tri vertical
Ordonne les étapes d'extension d'un module via le module augmentation :