Declaration merging occurs when TypeScript combines multiple declarations with the same name in the same file. Module augmentation extends a module defined elsewhere — without forking its source or duplicating its types.
Declaration merging xảy ra khi TypeScript kết hợp nhiều khai báo có cùng tên trong CÙNG một file. Module augmentation mở rộng một module được định nghĩa ở nơi khác — mà không cần fork hay sao chép source.
1// Declaration merging (same file — two interfaces merge)2interface Window {3 myProp: string;4}5interface Window {6 myOtherProp: number;7}8// Result: Window has both myProp and myOtherProp910// Module augmentation (separate file — extends an external module)11import "express"; // <-- This import makes the file a MODULE1213declare module "express" {14 interface Request {15 user?: AuthenticatedUser; // merged into Express's Request16 }17}import or export. A file without either is a script, not a module, and declare module inside it behaves differently.Sự khác biệt quan trọng nhất: file augmentation PHẢI là module file — có ít nhất một import hoặc export. Một file không có import/export là script file, không phải module file, và declare module trong đó hoạt động khác.
The most common use case is adding custom properties to Express's Request object after middleware has attached them. Without augmentation TypeScript errors on every req.user access.
Trường hợp sử dụng phổ biến nhất là thêm custom properties vào Express Request sau khi middleware đã attach chúng. Không có augmentation, TypeScript báo lỗi khi bạn truy cập req.user.
1// src/types/express.d.ts2import "express";34// Augment the SOURCE package, not the re-exporter "express"5declare module "express-serve-static-core" {6 interface Request {7 user?: {8 id: string;9 email: string;10 roles: string[];11 };12 requestId: string;13 startTime: number;14 }15}1617// Tip: find the right module with Go to Definition on Request in your IDE1// src/middleware/auth.ts2import type { Request, Response, NextFunction } from "express";3import jwt from "jsonwebtoken";45export function authenticate(6 req: Request,7 _res: Response,8 next: NextFunction9): void {10 const token = req.headers.authorization?.split(" ")[1];11 if (!token) return next();12To extend the global scope — Window, globalThis, or standalone global variables — from a module file, wrap the declarations in declare global { }. This is the only way to do it from a file that already has imports or exports.
Để mở rộng global scope (Window, globalThis, các biến global tự do) từ một module file, bao bọc các khai báo trong declare global { }. Đây là cách duy nhất để làm điều này từ một file có import hoặc export.
1// src/types/global.d.ts2export {}; // makes the file a module — required for declare global34declare global {5 interface Window {6 // Analytics injected via third-party script tag7 analytics: {8 track(event: string, props?: Record<string, unknown>): void;9 identify(userId: string, traits?: Record<string, unknown>): void;10 page(name?: string): void;11 };12 __APP_VERSION__: string;1// src/types/env.d.ts — type process.env strictly2export {};34declare global {5 namespace NodeJS {6 interface ProcessEnv {7 NODE_ENV: "development" | "test" | "production";8 DATABASE_URL: string;9 JWT_SECRET: string;10 PORT?: string;11 REDIS_URL?: string;12 // Any key not listed here is now a type error13 }14 }15}1617// Usage — fully typed18const env = process.env.NODE_ENV; // "development" | "test" | "production"19// process.env.UNKNOWN_KEY; // Error: Property does not existTypeScript can import non-JavaScript files when you declare an ambient module for that extension. This enables fully typed imports of .css, .svg, and image files.
TypeScript có thể import các file không phải JavaScript nếu bạn khai báo ambient module cho extension đó. Điều này cho phép import .css, .svg, .png với type safety đầy đủ.
1// src/types/assets.d.ts2export {};34// CSS Modules — each class name maps to a string5declare module "*.module.css" {6 const classes: { readonly [key: string]: string };7 export default classes;8}910declare module "*.module.scss" {11 const classes: { readonly [key: string]: string };12 export default classes;1// Typed JSON config — narrow the inferred shape for a specific pattern2declare module "*/config.json" {3 const config: {4 readonly apiUrl: string;5 readonly version: string;6 readonly features: Record<string, boolean>;7 };8 export default config;9}1011// Usage — fully typed, no 'any'12import config from "../config.json";13console.log(config.apiUrl); // string14console.log(config.features["darkMode"]); // booleanIf the file has no import or export, TypeScript treats it as a script. Declarations pollute the global scope unexpectedly and declare module may not merge correctly.
Nếu file không có import hoặc export, TypeScript coi nó là script file. Khai báo trong đó ảnh hưởng đến global scope theo cách không mong muốn — và declare module có thể không merge đúng.
1// WRONG — no imports or exports: TypeScript treats this as a SCRIPT2declare module "express-serve-static-core" {3 interface Request {4 user?: AuthUser; // may not merge correctly5 }6}78// CORRECT — the empty export makes this a MODULE9export {};1011declare module "express-serve-static-core" {12 interface Request {13 user?: AuthUser; // merges correctly into Express's Request14 }15}Many libraries re-export types from internal sub-packages. Augmenting the top-level package may silently fail if the interface actually lives in a different module.
Nhiều thư viện re-export types từ các sub-packages. Nếu bạn augment package tầng trên, TypeScript có thể không merge đúng vì khai báo thực sự nằm ở module khác.
1// WRONG — augmenting the re-exporter (may not merge)2declare module "express" {3 interface Request { user?: AuthUser; }4}56// CORRECT — augment where the interface is actually declared7declare module "express-serve-static-core" {8 interface Request { user?: AuthUser; }9}1011// Rule of thumb: use "Go to Definition" in your IDE on the type you want12// to augment to find the actual declaration's source module path1// types/next-auth.d.ts2import "next-auth";3import type { DefaultSession } from "next-auth";45declare module "next-auth" {6 interface Session {7 user: {8 id: string;9 role: "admin" | "user" | "moderator";10 organizationId: string;11 } & DefaultSession["user"]; // preserves name, email, image12 }1// types/fastify.d.ts2import "fastify";3import type { PrismaClient } from "@prisma/client";45declare module "fastify" {6 interface FastifyRequest {7 user: {8 id: string;9 email: string;10 permissions: Set<string>;11 };12 }1// Prisma generates types from your schema — augment to add application-layer fields2import type { User } from "@prisma/client";34// Pattern: type alias with intersection — simpler than augmenting @prisma/client5export type UserWithFullName = User & {6 fullName: string; // computed, not stored in DB7};89export function withFullName(user: User): UserWithFullName {10 return {11 ...user,12 fullName: `${user.firstName} ${user.lastName}`,Declaration merging kết hợp các khai báo cùng tên trong cùng file; module augmentation mở rộng module ở nơi khác
File augmentation PHẢI là module — thêm export {}; nếu không có import hoặc export nào khác
Dùng declare global { } để mở rộng Window, globalThis và biến tự do từ một module file
Augment module nguồn, không phải re-exporter — dùng Go to Definition để tìm đúng module
Ambient module declarations cho *.svg và *.css bật typed imports cho non-JS assets
NextAuth, Fastify và Prisma đều được mở rộng qua module augmentation trong các dự án thực tế
Module augmentation không thể xóa hoặc thay đổi các members hiện có — chỉ có thể thêm mới