之前学习了TypeScript的类型定义,我们都知道开发语言中的变量会有覆盖声明的情况,那么对于类型定义是不是也会有这种情况,那么该如何正确利用这种合并规则?在遇到多个类型定义的时候,我们又该如何处理?
了解类型合并规则,有助于我们定义类型,避免类型冲突。可以利用这种合并规则,更灵活的定义类型。
不同版本TypeScript,不同配置可能会导致合并差异,这里说明使用的版本为"typescript": "^5.9.3",开启了配置"strict": true,测试文件后缀为.ts。.d.ts文件可能更为宽松。
同名同类型合并
TypeScript仅支持接口interface、命名空间namespace、函数声明function同名合并。在声明解析阶段即完成合并。早于其他类型引用处理,比如交叉/联合类型。
对于type、 class 、enum等定义的同名类型都不能合并。
interface 接口声明合并
同一作用域下的同名接口会自动完成合并,无需额外语法。
合并特性:
- 属性、方法、索引签名均可合并。
- 同名属性、方法必须类型兼容,否则编译器报错。
- 同一作用域/模块下。
ts
interface Animal {
name: string;
}
interface Animal {
age: number;
}
// 实例必须包含name和age属性
const dog: Animal = {
name: "",
age: 0,
};
同属性,不同类型不兼容,编译器报错。
ts
interface Animal {
name: string;
}
// ❌ 后续属性声明必须属于同一类型。属性"name"的类型必须为"string",但此处却为类型"number"。
interface Animal {
name: number;
}
同属性同类型,不同修饰符不兼容,编译器报错。
ts
interface Animal {
name: string;
}
// ❌ 不同修饰符不兼容,编译器报错
interface Animal {
name?: string;
}
namespace 命名空间合并
同名的命名空间会自动合并内部导出的成员。仅export导出的成员会合并。
ts
namespace Utils {
export function getName() {
return "hboot";
}
}
namespace Utils {
export function getAge() {
return 18;
}
}
Utils.getName();
Utils.getAge();
不允许同名成员导出。
ts
namespace Utils {
export function getName() {
return "hboot";
}
}
namespace Utils {
// ❌ 成员已存在,不允许重复定义
export const getName = () => {
return 18;
};
}
function 函数声明合并
函数同名合并,我们称之为函数重载。TS编译器会按照倒序匹配,也就是后声明函数重载优先级高。
函数重载最后一个函数声明必须实现内部逻辑,并且参数数量、参数类型和返回值类型必须兼容。
仅使用
function声明的函数支持
ts
function getName(name: string): string;
function getName(age: number): number;
function getName(nameOrAge: string | number): string | number {
return nameOrAge;
}
getName("hboot");
getName(18);
interface接口中定义的方法也会形成重载。但是和普通的函数重载最后的函数实现参数、返回值类型定义有些不同。
ts
interface Animal {
getVal(name: string): string;
}
interface Animal {
getVal(age: number): number;
}
// ❌ 这样写编译器会直接报错,提示不能将类型 "string | number" 无法分配给类型 "string"。
const dog: Animal = {
getVal(value: string | number): string | number {
return value;
},
};
普通函数重载是满足其一即可;而接口中方法重载是必须精准匹配类型每一个类型。利用TS类型推导通过泛型参数锁定类型。
ts
// 利用了类型的自动推导,通过泛型参数 T 锁定输入类型及返回类型
const dog: Animal = {
getVal<T extends string | number>(value: T): T {
return value;
}
};
同名不同类型合并
同名不同类型的合并,主要是命名空间namespace + interface/class/function的合并,namespace可以提供静态属性、方法。
namespace+interface 合并
同名的namespace命名空间为interface接口扩展静态成员;接口提供类型约束。
ts
interface Animal {
name: string;
}
namespace Animal {
export const age = 18;
export function getName() {
return "hboot";
}
}
const dog: Animal = {
name: "hboot",
};
Animal.getName();
同名的属性、方法不会冲突,因为Animal命名空间直接通过空间名访问;interface接口需要实例化后的实例访问。它们之间实际上只有名称是相同的,属性之间没有合并。
namespace+class 合并
同名的namespace命名空间为class类扩展静态成员;类声明必须在命名空间的声明必之前,命名空间不能声明类已有的成员。
ts
class Animal {
name: string;
static age: number;
constructor(name: string) {
this.name = name;
}
}
namespace Animal {
export const name = "hboot";
// ❌ 此处扩展静态成员 age 报错,类中已存在 age 静态成员
export const age = 18;
export function getName() {
return "hboot";
}
}
const dog: Animal = new Animal("admin");
Animal.getName();
// hboot
Animal.name;
// admin
dog.name;
namespace+function 合并
同名的namespace命名空间为function函数扩展静态成员。函数保持自身的可调用能力。
ts
function speak(name: string) {
return "Hello World! " + name;
}
namespace speak {
// ❌ 此处无法覆盖 函数的 name 属性;name 是只读属性
// ❌ 类型校验没有报错,但运行时因为只读而报错
export const name = "hboot";
export function getName() {
return "hboot";
}
}
speak("hboot");
speak.name;
speak.getName();
扩展的静态成员最好不要覆盖函数本身的属性,比如name、length等。这些只读属性无法被覆盖,在运行时会报错。
interface+class 合并
同名的interface接口为class类扩展实例成员。类继承接口的属性、方法,实例必须同时满足接口和类的约束。
合并特性:
- 接口的必选属性,在类中必须显式实现,否则执行报错。
- 同名属性,必须类型兼容,否则执行报错。
ts
interface Animal {
name: string;
getName(): string;
}
class Animal {
age: number;
constructor(age: number, name: string) {
this.age = age;
this.name = name;
}
// 必须显示实现 接口 的方法成员
getName() {
return this.name;
}
}
const dog: Animal = new Animal(18, "hboot");
dog.age;
// 类中需通过构造函数
dog.name;
// ❌ 如果类没有显示实现;智能提示存在方法,实际调用会报错。
dog.getName();
除了手动赋值扩展属性外,可以通过public修饰符自动生成
ts
class Animal {
constructor(age: number, public name: string) {
this.age = age;
// 无需手动赋值
// this.name = name;
}
const dog: Animal = new Animal(18, "hboot");
// ...
}
显示类型合并
上述的同名同类型、同名不同类型合并实际最终也是属性的合并。对于非同名属性合并则是扩展;同名属性则有一些合并规则。
对于不同类型之间的同名属性合并都有自己的规则,比如:interface+class 合并要求属性类型兼容;namespace+class 合并要求命名空间不能包含类已有的成员。
通过手动将一些类型合并到一个类型中,例如交叉类型&和联合类型|
交叉类型& 关系合并
交叉类型将多个类型合并为一个新类型。新类型必须满足所有类型约束。
合并特性:
- 不同名属性合并为属性并集。保留属性修饰符。
- 同名属性取兼容类型,对于修饰符
?,存在必选时则属性必选;修饰符readonly,存在属性可修改时则属性可修改。
ts
// 不兼容类型 never
type A = string & number;
// 不同名属性并集
type B = { name: string } & { age: number };
// 可选 ? 修饰符, 兼容类型 name 为必选属性
type C = { name?: string } & { name: string };
// 只读 readonly 修饰符, 兼容类型 age 为可修改
type D = { readonly age: number } & { age: number };
联合类型| 关系合并
将多个类型组合为一个新类型。新类型只需要满足其中一个类型约束。
合并特性:
- 仅能访问公共属性。需通过类型守卫收窄类型后才能访问非公共属性。
- 完全一致的类型自动去重。
ts
type A = {
name: string;
age: number;
};
type B = {
name: string;
address: string;
};
type C = A | B;
// 满足其中一个类型约束
const c: C = {
name: "hboot",
age: 18,
};
// 访问非公共属性
function viewC(data: C) {
if ("age" in data) {
return data.age;
}
return data.address;
}
.d.ts中的声明合并
.d.ts文件和.ts文件的合并核心规则一致。在执行时机、作用域、编译行为上有些不一样。
-
.d.ts文件中仅有类型声明,无实际代码实现,仅用于TS类型检验。所以不同于.ts文件。它会在类型校验阶段早期执行,优先合并全局/模块类型;而.ts文件是在编译阶段执行。 -
.d.ts类型优先级低,能被.ts文件显示类型声明覆盖。 -
.d.ts跨文件同名声明合并(无import/export)。.ts仅在同一个文件中同名声明合并。
declare
declare 主要作用存在性声明,告诉TypeScript编译器无需生成对应的代码。
- 全局变量/函数声明,比如:外部加载的
js文件,挂载到window上的变量。 - 扩展已有类型(与同名接口/命名空间合并)
- 声明模块(非TS模块,比如
.css或.png等静态资源)
declare 扩展已有类型合并规则与.ts文件的合并核心规则一致.
ts
interface Animal {
name: string;
getName(): string;
}
declare interface Animal {
age: number;
}
.ts 模块中没有import/export时,通过declare声明为全局作用域。如果存在import/export则需要通过declare global扩展全局作用域。