TypeScript permits multiple declarations with the same name in the same scope, as long as they belong to compatible entity kinds. The compiler merges them into a single definition. This is purely a type-system feature — no JavaScript runtime construct is involved.
TypeScript cho phép nhiều khai báo cùng tên tồn tại trong cùng một phạm vi, miễn là chúng thuộc các loại thực thể tương thích. Trình biên dịch sẽ 'ghép' chúng thành một định nghĩa duy nhất. Điều này khác hoàn toàn với JavaScript thuần túy — đây là tính năng chỉ dành cho hệ thống kiểu.
1// Three separate interface declarations…2interface Box {3 height: number;4}56interface Box {7 width: number;8}910interface Box {11 depth: number;12}1314// …merged into one15const cube: Box = { height: 10, width: 10, depth: 10 }; // all three requiredInterface merging is the most common case. TypeScript unions all members from every declaration with the same name. This is the mechanism for extending an existing interface without touching its source file.
Ghép interface là trường hợp phổ biến nhất. TypeScript hợp nhất tất cả các thành viên từ mọi khai báo trùng tên. Đây là cách để mở rộng một interface đã tồn tại mà không cần chỉnh sửa file gốc.
1// lib/types.ts — original declaration2interface PaginatedResponse<T> {3 data: T[];4 total: number;5 page: number;6}78// extensions/types.ts — added later, same name9interface PaginatedResponse<T> {10 cursor?: string; // new optional field11 hasMore: boolean; // new required field12}Thứ tự của các khai báo ảnh hưởng đến thứ tự overload của các thành viên hàm. Khai báo sau được ưu tiên hơn khai báo trước trong danh sách overload. Với non-function members, các kiểu phải khớp hoàn toàn — không có union ngầm định.
Namespaces merge too. TypeScript combines all exported members from declarations with the same name. This is commonly used to split a namespace across files or add sub-types and constants to an existing one.
Các namespace cũng có thể được ghép lại. TypeScript hợp nhất tất cả các thành viên được export từ các khai báo cùng tên. Điều này thường được dùng để tổ chức code theo file hoặc thêm hằng số, kiểu con vào một namespace hiện có.
1// validators/core.ts2namespace Validators {3 export interface StringValidator {4 isAcceptable(s: string): boolean;5 }6}78// validators/zip.ts9namespace Validators {10 const numberRegexp = /^[0-9]+$/; // not exported — private to this file1112 export class ZipCodeValidator implements StringValidator {13 isAcceptable(s: string): boolean {14 return s.length === 5 && numberRegexp.test(s);15 }16 }17}1819// Usage — both members visible20const v: Validators.StringValidator = new Validators.ZipCodeValidator();Non-exported members of a namespace block are visible only within that block. This creates private namespace-level state shared among functions in the same file without leaking outward.
Các thành viên không được export trong một namespace block chỉ hiển thị trong chính block đó. Đây là cách tạo 'private namespace state' — dữ liệu được chia sẻ giữa các hàm trong cùng block nhưng ẩn với bên ngoài.
Module augmentation lets you extend third-party module types without forking or modifying node_modules. It is the standard technique for patching types onto libraries with incomplete or outdated definitions.
Module augmentation cho phép bạn mở rộng kiểu của một module bên thứ ba mà không cần fork hay sửa đổi node_modules. Đây là kỹ thuật chuẩn để 'patch' kiểu cho các thư viện chưa cập nhật type definitions.
1// augment-axios.d.ts23// IMPORTANT: this file must be a module (not a script)4// so add at least one import or export5export {};67declare module "axios" {8 interface AxiosRequestConfig {9 // Inject a custom traceId into every request config10 traceId?: string;11 retries?: number;12 }import or export. Without it TypeScript treats the file as an ambient script and the declare module creates a brand-new module rather than extending the existing one.File augmentation phải là một module — nó cần ít nhất một câu lệnh import hoặc export ở cấp cao nhất. Nếu không có, TypeScript sẽ hiểu đây là một ambient script và khai báo declare module sẽ tạo ra một module hoàn toàn mới thay vì mở rộng module hiện có.
You can declare an interface with the same name as a class to add members to the class type. This is used to reconstruct "type-only mixins" or to safely annotate class members that decorators are known to inject.
Bạn có thể khai báo một interface trùng tên với một class để thêm các thành viên vào class đó. Đây thường được dùng để tái tạo 'mixins kiểu chỉ' hoặc để thêm các thành viên được khai báo bởi decorator vào class một cách an toàn.
1class ApiClient {2 baseUrl: string;34 constructor(baseUrl: string) {5 this.baseUrl = baseUrl;6 }78 get(path: string): Promise<unknown> {9 return fetch(this.baseUrl + path).then((r) => r.json());10 }11}12The most common pattern in Express/Node projects is attaching a user property to the Request object after authentication.
Pattern phổ biến nhất trong các dự án Express/Node là thêm thuộc tính user vào đối tượng Request sau khi xác thực.
1// src/types/express/index.d.ts2import type { User } from "../../models/user";34declare global {5 namespace Express {6 interface Request {7 user?: User;8 requestId: string;9 }10 }11}121// types/next-auth.d.ts2import type { DefaultSession } from "next-auth";34declare module "next-auth" {5 interface Session {6 user: {7 id: string;8 role: "admin" | "editor" | "viewer";9 } & DefaultSession["user"];10 }1112 interface JWT {When merged interfaces contain function members, TypeScript places overloads from later declarations first in the merged overload list. This reverses the order compared with standard function overloads and can produce unexpected resolution.
Khi ghép interface có các function members, TypeScript đặt các overload từ khai báo sau lên trước trong danh sách overload của interface được ghép. Điều này đảo ngược thứ tự so với overload thông thường, có thể gây ra hành vi bất ngờ.
1interface Formatter {2 format(value: number): string; // declared first — resolves LAST3}45interface Formatter {6 format(value: string): string; // declared later — resolves FIRST7}89// Effective merged signature (later declarations win higher priority):10// format(value: string): string;11// format(value: number): string;1213declare const f: Formatter;14const result = f.format(42); // string — but the number overload is lower priorityA declare module 'name' in a script-mode file (no imports/exports) creates a brand-new ambient module. This is powerful for typing non-TypeScript files but is easily confused with augmentation.
Khai báo declare module 'name' trong một file script (không có import/export) tạo ra một module ambient hoàn toàn mới. Đây là công cụ mạnh mẽ để khai báo kiểu cho các file không phải TypeScript, nhưng dễ nhầm lẫn với module augmentation.
1// ambient module for SVG imports (script-mode file — no imports)2declare module "*.svg" {3 const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;4 export default ReactComponent;5}67// wildcard for CSS modules8declare module "*.module.css" {9 const styles: Record<string, string>;10 export default styles;11}1213// augmentation (module-mode file — has export {})14export {};15declare module "some-lib" {16 interface ExistingInterface {17 newField: string; // extends, not replaces18 }19}Interface merging ghép tất cả khai báo cùng tên thành một — không cần sửa file gốc
Namespace merging kết hợp các thành viên export từ nhiều block cùng tên
declare module in a module-mode fileModule augmentation mở rộng kiểu của module bên thứ ba bằng declare module trong một module-mode file
Ghép interface với class cho phép khai báo thành viên inject bởi decorator mà không có chi phí runtime
Khai báo sau có ưu tiên overload cao hơn với function members — thứ tự quan trọng
File augmentation phải là một module (có ít nhất một import/export cấp cao nhất)