10 - Déclaration merging et module augmentation
Ce que tu vas apprendre
- Comment TypeScript fusionne automatiquement les déclarations du meme nom
- Comment etendre les types d'une lib tierce avec module augmentation
- Comment ajouter des types globaux (Window, process.env)
- Les cas d'usage concrets : Express, Prisma, env variables
Prerequisites
Avoir lu les articles sur les interfaces vs types et les generics.
Le problème des types qu'on ne contrôle pas
Quand tu utilises Express, le type de req est Request. Mais apres ton middleware d'authentification, req a une propriété user que les types d'Express ne connaissent pas :
typescriptapp.use(authMiddleware)
app.get("/profile", (req, res) => {
console.log(req.user.id) // ❌ Property 'user' does not exist on type 'Request'
})
Tu ne peux pas modifier les types d'Express dans node_modules. Et any n'est pas une option. La solution : déclaration merging.
Déclaration merging
TypeScript fusionne automatiquement deux déclarations qui ont le meme nom dans le meme scope. Ca fonctionne avec les interfaces (pas les types) :
typescriptinterface User {
name: string
}
interface User {
email: string
}
// Resultat : User a les deux proprietes
const user: User = {
name: "Nicolas",
email: "n@n.dev"
}
Les deux déclarations sont fusionnees. C'est spécifique aux interfaces — les type aliases ne peuvent pas etre fusionnes.
Ca fonctionne aussi avec les namespaces, les enums, et entre interfaces et classes. Mais le cas d'usage le plus courant, c'est l'extension de types de libs tierces.
Module augmentation
Pour etendre les types d'un module npm, tu declares un module avec le meme nom :
typescript// types/express.d.ts
import { User } from "../models/user"
declare module "express-serve-static-core" {
interface Request {
user: User
}
}
Maintenant req.user est type dans tous tes handlers Express :
typescriptapp.get("/profile", (req, res) => {
console.log(req.user.id) // ✅ type: User
})
Le declare module "express-serve-static-core" dit a TypeScript : "ajoute ces déclarations au module existant". Les interfaces du meme nom sont fusionnees.
Trouver le bon module a augmenter
Le piège classique : augmenter le mauvais module. Pour Express, le type Request est dans express-serve-static-core, pas dans express. Comment le savoir ?
- Ouvre
node_modules/@types/express/index.d.ts - Regarde d'ou
Requestest importe - Augmente ce module-la
Ou utilise ton IDE : fais Ctrl+Click sur Request et regarde dans quel module il est déclaré.
Global augmentation
Pour ajouter des types a l'objet global (window, globalThis, process) :
typescript// types/global.d.ts
declare global {
interface Window {
dataLayer: Record<string, unknown>[]
gtag: (...args: unknown[]) => void
}
}
export {} // necessaire pour que le fichier soit un module
Le declare global {} dit a TypeScript : "ajoute ces déclarations au scope global". Le export {} est obligatoire pour que TypeScript traite le fichier comme un module (sinon declare global ne fonctionne pas).
typescriptwindow.dataLayer.push({ event: "page_view" }) // ✅ type
window.gtag("event", "purchase", { value: 99 }) // ✅ type
Variables d'environnement typees
Un pattern que j'utilise sur paltemps.fr :
typescript// types/env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test"
DATABASE_URL: string
ADMIN_PASSWORD: string
ANTHROPIC_API_KEY: string
PORT?: string
}
}
}
export {}
Maintenant process.env.DATABASE_URL est type string (pas string | undefined) et process.env.NODE_ENV n'accepte que les trois valeurs définies. L'autocompletion propose les variables disponibles.
Augmenter des libs courantes
Prisma : ajouter des méthodes custom
typescript// types/prisma.d.ts
import { PrismaClient } from "@prisma/client"
declare module "@prisma/client" {
interface PrismaClient {
$softDelete<T>(model: string, id: string): Promise<T>
}
}
Elysia / Hono : context custom
typescriptdeclare module "elysia" {
interface Context {
user: AuthUser
requestId: string
}
}
Vue : propriétés globales
typescriptdeclare module "vue" {
interface ComponentCustomProperties {
$translate: (key: string) => string
}
}
Interface merging vs type : pourquoi ca compte
C'est une des raisons pour lesquelles les interfaces et les types ne sont pas interchangeables. Les types ne se fusionnent pas :
typescript// ❌ Erreur : Duplicate identifier
type Config = { port: number }
type Config = { host: string }
// ✅ Fonctionne : merge
interface Config { port: number }
interface Config { host: string }
Si tu publies une lib et que tu veux que tes utilisateurs puissent etendre tes types, utilise des interfaces pour les types extensibles (options, contexte, config) et des types pour les types fermes (unions, intersections, utilitaires).
Fichiers .d.ts
Les déclarations d'augmentation vont généralement dans des fichiers .d.ts dans un dossier types/ a la racine du projet. Le tsconfig doit inclure ce dossier :
json{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src/**/*", "types/**/*"]
}
Ou plus simplement, assure-toi que les fichiers .d.ts sont dans le scope include du tsconfig.
Pieges
Le fichier doit etre un module
Si ton fichier .d.ts n'a pas d'import ou d'export, TypeScript le traite comme un script global. Le declare module ne fera pas ce que tu attends. Ajoute export {} en bas du fichier.
Augmentation vs remplacement
L'augmentation ajoute des propriétés. Elle ne remplace pas les existantes. Si tu veux changer le type d'une propriété existante, c'est plus complique et généralement déconseillé.
Conflits entre augmentations
Si deux fichiers augmentent la meme interface avec des propriétés du meme nom mais de types différents, tu auras une erreur de compilation. Verifie que tes augmentations ne se contredisent pas.
Résumé
- TypeScript fusionne automatiquement les interfaces du meme nom (déclaration merging)
declare module "nom"etend les types d'un module npm sans modifier ses fichiersdeclare global {}ajoute des types au scope global (Window, process.env)- Les interfaces sont extensibles, les types ne le sont pas — utilise des interfaces pour les types publics extensibles
- Les fichiers d'augmentation doivent etre des modules (ajouter
export {}si pas d'import)
Article précédent : 09 - Overloads
Article suivant : 11 - Symbols, unique symbol et opaque types