Module augmentation y declaration merging en TypeScript | Nicolás Garzón
TypeScript
Copiar import { Observable } from "./observable.js" ;
Observable. prototype. map = function ( transform) {
} ;
declare module "./observable.js" {
interface Observable< T > {
map < TResult> (
transform : ( value: T ) => TResult,
) : Observable< TResult> ;
}
}
Modificación runtime del prototipo.
Augmentation estática para el checker.
El specifier dentro de declare module se resuelve igual que un import desde ese archivo.
TypeScript
Copiar declare module "library" { } TypeScript
Copiar declare module "./library.js" { } Amplían identidades distintas.
TypeScript
Copiar export { } ;
declare module "library" {
interface Client {
traceId? : string ;
}
} Sin top-level import/export, la sintaxis podría interpretarse como ambient module nuevo en lugar de augmentation.
Normalmente se amplían declaraciones con nombres exportados existentes, especialmente interfaces.
No puedes añadir una nueva exportación top-level completamente desconocida mediante augmentation como si modificaras el namespace del módulo libremente.
Tampoco puedes ampliar directamente el default export porque no tiene un nombre exportado estable para fusionar.
TypeScript
Copiar
export interface PluginRegistry { } TypeScript
Copiar
import "./core.js" ;
declare module "./core.js" {
interface PluginRegistry {
analytics: AnalyticsPlugin;
}
} El plugin registra además su implementación runtime donde corresponda.
Un patrón habitual amplía request/context:
TypeScript
Copiar import "express-serve-static-core" ;
declare module "express-serve-static-core" {
interface Request {
user? : AuthenticatedUser;
}
} El middleware debe asignar request.user en runtime. Si siempre lo declaras requerido pero algunas rutas no ejecutan middleware, el tipo miente.
TypeScript
Copiar export { } ;
declare global {
interface Array < T > {
first ( ) : T | undefined ;
}
} TypeScript
Copiar Array . prototype. first = function ( ) {
return this [ 0 ] ;
} ; Modificar prototipos globales puede colisionar con otras librerías o estándares futuros. Evítalo en aplicaciones normales.
TypeScript
Copiar interface User {
id: string ;
}
interface User {
email: string ;
} No requiere declare module porque ambas están en el mismo scope.
En un ambient module que no es augmentation, imports internos pueden referir a tipos externos. Mover un import al top-level cambia el archivo a módulo y puede cambiar completamente el significado.
TypeScript
Copiar type Config = {
value: string ;
} ; Un type alias no participa en declaration merging. Una librería que quiere una registry abierta suele exponer interface.
Augmentation acopla tu proyecto a nombres internos y orden de setup.
Un adapter puede ser más explícito:
TypeScript
Copiar function createAuthenticatedRequest (
request: Request,
user: User,
) : AuthenticatedRequest {
return { request, user } ;
} Una augmentation depende de la forma de la librería. Una actualización puede renombrar el módulo o miembro y dejar la declaración obsoleta. Mantén tests de tipos y runtime.
TypeScript
Copiar
export interface RouteMetadata { } TypeScript
Copiar
import "./router.js" ;
declare module "./router.js" {
interface RouteMetadata {
requiredPermission? : Permission;
}
}
Añadir solo el tipo sin implementación runtime.
Usar el specifier incorrecto.
Olvidar convertir el archivo en módulo.
Declarar required un miembro que no siempre existe.
Intentar augmentar un type alias o default export.
Modificar prototipos globales por comodidad.
Crear augmentations que dependen de internals no públicos.
No revisar compatibilidad al actualizar la librería.
Module augmentation fusiona declaraciones de un módulo existente.
El specifier identifica exactamente qué módulo se amplía.
El archivo debe ser módulo para evitar crear un ambient module nuevo.
La capacidad runtime debe existir.
Interfaces abiertas facilitan plugins.
Type aliases no se fusionan.
Wrappers y adapters suelen ser más explícitos.
Augmentations necesitan pruebas frente a versiones de dependencias.
¿Por qué declarar Request.user no hace que Express lo asigne?
Respuesta Porque la augmentation solo cambia el contrato estático. Un middleware debe crear realmente esa propiedad durante ejecución.
JavaScript interop, allowJs, checkJs y JSDoc muestra una migración gradual sin perder el runtime existente.