同名声明会自动合并,这是给第三方库补类型、扩展全局对象的基础。但不是所有声明都能合并,规则需要记清楚。
能合并的声明
1// 声明合并的完整规则
2
3// ---- 1. interface 合并:成员累加 ----
4interface Box { width: number }
5interface Box { height: number }
6const box: Box = { width: 1, height: 2 };
7
8// 同名同类型成员可以重复
9interface Dup { a: string }
10interface Dup { a: string } // ✓ 类型相同,允许
11
12// 同名不同类型会报错
13interface Conflict { b: number }
14// @ts-expect-error TS2717: 后续属性声明必须类型相同
15interface Conflict { b: string }
16
17// ---- 2. 方法重载:后声明的优先 ----
18interface Calc { add(x: string): string }
19interface Calc { add(x: number): number }
20
21const calc = { add: (x: string | number) => x } as Calc;
22const added = calc.add(1); // number(后声明的签名优先)
23
24// ---- 3. namespace 合并 ----
25namespace Util { export const a = 1 }
26namespace Util { export const b = 2 }
27// Util 同时有 a 和 b
28
29// ---- 4. namespace 与 class 合并 ----
30class Widget { render() { return "w" } }
31namespace Widget { export const version = "1.0" }
32// Widget 既是类,也带静态成员 version
33const w = new Widget();
34const ver = Widget.version;
35
36// ---- 5. namespace 与 function 合并 ----
37function greet() { return "hi" }
38namespace greet { export const times = 1 }
39greet();
40const t = greet.times;
41
42// ---- 6. namespace 与 enum 合并:给枚举加方法 ----
43enum Color { Red, Green }
44namespace Color {
45 export function name(c: Color): string {
46 return c === Color.Red ? "red" : "green";
47 }
48}
49const colorName = Color.name(Color.Red);
50
51// ---- 7. type 不能合并 ----
52// 重复的 type 声明会报 TS2300,且错误同时报在**两处**声明上:
53// error TS2300: Duplicate identifier 'T1'. (两行都报)
54// 所以这里用注释演示,不写进代码:
55// type T1 = { a: 1 };
56// type T1 = { b: 2 }; // ← 两行都报 TS2300
57type T1 = { a: 1 }; // 只保留一份
58
59// 用 interface 替代
60interface Merged { a: 1 }
61interface Merged { b: 2 }
62const merged: Merged = { a: 1, b: 2 };
63
64// ---- 8. 合并的陷阱:顺序影响重载 ----
65interface Order1 { f(x: string): "s" }
66interface Order1 { f(x: number): "n" }
67// 后声明的(number 版本)优先
68
69// ---- 9. 重复的 const/let 声明不允许 ----
70// 重复声明 const 会报 TS2451,错误报在声明处:
71// const dupConst = 1;
72// const dupConst = 2; // ← error TS2451: Cannot redeclare block-scoped variable
73const dupConst = 1;
74// 重复声明 const 会报 TS2451,同样报在声明处:
75// const dupConst = 1;
76// const dupConst = 2; // ← error TS2451: Cannot redeclare block-scoped variable
77void dupConst;
78
79// 但 var 可以重复声明
80var dupVar = 1;
81var dupVar = 2;
82
83console.log(box, added, Util.a, Util.b, w.render(), ver, greet(), t, colorName, merged);
84const dupValue: Dup = { a: "x" };
85console.log(dupValue, dupVar);
86const conflict = { b: 1 } as unknown as Conflict;
87console.log(conflict);
88const t1: T1 = { a: 1 };
89// 重载接口的实现需要用 any 或断言(因为返回类型依赖入参)
90const order = {
91 f: (x: string | number) => (typeof x === "string" ? "s" : "n"),
92} as Order1;
93console.log(t1, order.f("a"), order.f(1));
1{ b: 1 }
2{ a: 1 } s n
interface 合并
1interface Box { width: number }
2interface Box { height: number } // 自动合并
3
4const box: Box = { width: 1, height: 2 };
同名同类型的成员可以重复,类型不同则报错:
1interface Conflict { b: number }
2interface Conflict { b: string }
3// error TS2717: Subsequent property declarations must have the same type.
namespace 合并
1namespace Util { export const a = 1 }
2namespace Util { export const b = 2 }
3// Util 同时有 a 和 b
namespace 与 class / function / enum 合并
这让类、函数、枚举获得"额外成员":
1class Widget { render() { return "w" } }
2namespace Widget { export const version = "1.0" }
3
4const w = new Widget();
5Widget.version; // ✓ 类获得了静态成员
1enum Color { Red, Green }
2namespace Color {
3 export function name(c: Color): string { ... }
4}
5
6Color.name(Color.Red); // ✓ 枚举获得了方法
方法重载顺序
多个同名 interface 声明方法时,后声明的签名优先:
1interface Calc { add(x: string): string }
2interface Calc { add(x: number): number }
3
4const calc = { add: (x: string | number) => x } as Calc;
5calc.add(1); // number(后声明的优先)
不能合并的声明
type 不能合并:
1type T1 = { a: 1 };
2type T1 = { b: 2 };
3// error TS2300: Duplicate identifier 'T1'. ← 两行都报
注意错误同时报在两处声明上,这和其他错误不同。
const/let 也不能重复声明:
1const x = 1;
2const x = 2;
3// error TS2451: Cannot redeclare block-scoped variable 'x'.
(var 可以重复声明,这是 JavaScript 的遗留行为。)
interface这是 interface 与 type 里"优先用 interface"的一条实际理由。类型别名更灵活,但失去了可合并性。
给第三方库补类型
两种场景,写法不同。
场景一:模块不存在
用 declare module,写在全局脚本文件(无 import/export)里:
1// 环境声明:给不存在的模块补类型
2// 全局脚本文件(无 import/export)
3declare module "legacy-lib" {
4 export function doSomething(input: string): number;
5 export const VERSION: string;
6}
场景二:扩展全局作用域
用 declare global,必须写在模块文件(有 import/export)里:
1// 扩展全局作用域:必须是模块文件(有 import/export)
2export {};
3
4declare global {
5 interface Array<T> {
6 first(): T | undefined;
7 }
8 interface String {
9 isBlank(): boolean;
10 }
11}
declare module "xxx"→ 全局脚本文件(无 import/export)declare global→ 模块文件(有 import/export,通常配export {})
写错位置会报:
1error TS2664: Invalid module name in augmentation, module 'xxx' cannot be found.
这条错误消息很误导——它说的是"找不到模块",但真正的问题是文件类型不对(在模块文件里写 declare module 会被当成模块扩充而不是模块声明)。
只提供类型,不提供实现
这是最容易忽略的一点:
1// 使用声明合并提供的类型
2//
3// 关键:declare global 只提供**类型**,运行时实现要自己补。
4// 所以补实现的代码必须在**使用之前**执行。
5
6// ---- 1. 先补运行时实现 ----
7Array.prototype.first = function <T>(this: T[]): T | undefined {
8 return this[0];
9};
10String.prototype.isBlank = function (this: string): boolean {
11 return this.trim().length === 0;
12};
13
14// ---- 2. 再使用(类型来自 global-extend.ts / use-augment.d.ts)----
15const arr: number[] = [1, 2];
16const f: number | undefined = arr.first();
17
18const s: string = "x";
19const blank: boolean = s.isBlank();
20
21// ---- 3. 模块声明(来自 ambient.d.ts)只做类型验证 ----
22// 运行时 legacy-lib 不存在,所以只取类型不调用
23import type { doSomething } from "legacy-lib";
24const typeCheck: typeof doSomething = (input: string) => input.length;
25
26console.log(arr.first(), " ".isBlank(), "x".isBlank(), f, blank, typeCheck("abc"));
11 true false 1 false 3
declare global 和 declare module 只提供类型。运行时实现要自己补,而且必须在首次使用之前执行:
1// ✓ 先补实现
2Array.prototype.first = function <T>(this: T[]): T | undefined {
3 return this[0];
4};
5
6// ✓ 再使用
7const arr: number[] = [1, 2];
8arr.first();
如果顺序反了,会得到运行时的 TypeError: arr.first is not a function——类型检查完全通过,但代码跑不起来。这是 类型擦除 的直接后果。
给 Array.prototype 加方法会影响所有数组,包括第三方库里的。这可能导致:
- 与未来的 JavaScript 标准冲突(标准加了同名方法时行为可能不同)
- 与第三方库的同名扩展冲突
for...in遍历时出现意外属性
现代实践倾向于用独立函数(first(arr))而不是原型扩展。原型扩展主要用在无法改动调用点的场景。
声明文件的位置约定
常见做法是建一个 types/ 目录:
1types/
2├── legacy-lib.d.ts # declare module "legacy-lib"
3└── global.d.ts # declare global / 全局接口
然后确保 tsconfig.json 的 include 覆盖它:
1{ "include": ["src", "types"] }
详见 .d.ts 与第三方类型。
速查
| 声明 | 能否合并 | 用途 |
|---|---|---|
interface | ✓ | 扩展接口、给库加成员 |
namespace | ✓ | 组织代码、附加静态成员 |
class + namespace | ✓ | 类加静态成员 |
function + namespace | ✓ | 函数加属性 |
enum + namespace | ✓ | 枚举加方法 |
declare module | ✓ | 模块声明(全局脚本文件) |
declare global | ✓ | 全局扩展(模块文件) |
type | ✗ | — |
const/let | ✗ | — |
相关
- interface 与 type —— 为什么需要可合并性
- .d.ts 与第三方类型 —— 声明文件的组织
- 类型擦除 —— 为什么声明不提供运行时