本文深入解析 TypeScript 中的交叉类型(Intersection Types),通过 & 运算符合并多个类型。文章涵盖对象字面量验证差异、接口与 type 的交集求法、模拟继承机制,重点讨论了属性类型冲突时生成 never 类型的场景,以及函数类型取交集时参数取并集的特殊规则,帮助开发者规避类型定义陷阱。
在 TypeScript 类型系统中,交叉类型(Intersection Types)允许将多个类型合并为一个单一类型。这种机制通过 & 符号实现,旨在创建同时具备所有参与类型特征的复合类型。
type A = string & number; // never, 一般我们都不会把交集用在基本类型,而是用在对象上
type 有右手的人 = {
right: string;
};
type C = 有左手的人 | 有右手的人;
type D = 有左手的人 & 有右手的人;
const d: D = {
left: 'yes', // 会报错,缺少right
};
为了直观理解交叉类型的行为,我们可以考察一个关于“左手”与“右手”属性的示例。当尝试定义一个既拥有左手又拥有右手的类型时,如果两个源类型中没有共同的属性交集,或者属性类型存在根本性冲突,TypeScript 编译器会如何处理这种情况?
// 场景一. 直接初始化赋值
type 有左手的人 = {
left: string;
};
const a: 有左手的人 = {
left: '一米八',
right: '一米五', // 这么写会报错
};
// 场景二. 先声明好对象,再赋值
type 有左手的人 = {
left: string;
};
const b = {
left: '一米八',
right: '一米五',
};
const a: 有左手的人 = b; // 这么写不报错通过上述示例可以看出,交叉类型并非总是能生成有效的对象类型。当参与交叉的类型之间没有任何共同属性,或者属性类型互斥时,结果可能是一个 never 类型,这意味着该类型在逻辑上无法实例化任何值。
在实际编码场景中,直接对象字面量声明会触发严格的属性匹配验证,而先声明变量再赋值的间接方式可能会绕过这种即时验证。因此,前者会因缺少必需属性而报错,后者则不会立即报错,这体现了 TypeScript 类型检查在不同赋值方式下的差异。
除了 type 别名,interface 同样支持求交集操作。通过将多个接口使用 & 符号连接,可以创建一个包含所有接口成员的新类型。这种方式常用于组合不同的功能模块或数据模型,确保新类型具备所有源接口的属性。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
const CC: ColorfulCircle = {
color: 'red',
radius: 11,
};利用 & 运算符可以实现类似面向对象中继承的效果。通过交叉类型,可以将基础类型(如 Person)与扩展属性(如 User 特有的 id 和 email)合并,从而生成一个包含所有属性的复合类型。这种方式比传统的 class 继承更灵活,适用于函数式编程风格。
type Person = {
name: string;
age: number;
};
type User = Person & {
id: number;
email: string;
};
const u: User = {
name: 'Jack',
age: 18,
id: 1,
email: 'Jack@alibaba.com',
};在使用 & 进行类型交叉时,如果两个源类型包含同名但类型不兼容的属性,就会发生属性冲突。此时,TypeScript 会尝试寻找这两个类型的交集,如果找不到兼容的类型,结果属性将被推断为 never。
当发生属性冲突时,交叉类型会尝试求交集。如果源类型的属性类型完全互斥(例如一个是 string,另一个是 number),最终该属性的类型将变为 never。这意味着在实例化对象时,必须提供一个 never 类型的值,这在逻辑上是不可能的,从而导致类型错误。
// 场景一
type Person = {
name: string;
age: number;
id: string;
};
type User = Person & {
id: number;
email: string;
};
const u: User = {
name: 'Jack',
age: 18,
id: 1 as never, // 这个变成了never,且必须存在
email: 'Jack@alibaba.com',
};
// 场景二 User是never, 这是一种特殊的场景
type Person = {
id: 'A';
name: string;
age: number;
};
type User = Person & {
id: 'B';
email: string;
};当 interface 遇到属性冲突时,编译器会直接报错,提示类型不兼容。这与 type 的行为不同,type 在定义时可能不会立即报错,而是生成一个包含 never 属性的复杂类型,直到实际使用时才暴露问题。
interface Person {
id: string;
name: string;
}
// 会报错 接口“User”错误扩展接口“Person”。属性“id”的类型不兼容。不能将类型“number”分配给类型“string”。
interface User extends Person {
id: number;
email: string;
}由此可见,在编写可扩展的类型时,interface 更为合适,因为它会在类型定义阶段就发现逻辑错误。如果使用 type 且定义不合逻辑,编译器只会生成一个包含 never 属性的类型,问题往往在后续使用时才会被发现,增加了调试难度。
交叉类型不仅适用于对象和接口,还可以应用于函数类型。对两个函数类型进行交叉运算,会生成一个新的函数类型,该函数需要同时满足所有源函数的签名要求。
两个函数类型求交集时,最终得到的函数其参数类型将是所有源函数参数类型的并集。这意味着新函数必须能够接受任何源函数所接受的参数类型。这一结论将在后续章节详细展开,此处先展示基本结果。
type A = {
method: (a: number) => void;
};
type B = {
method: (a: string) => void;
} & A;
const b: B = {
method(a) {
a; // number | string
},
};交叉类型主要用于处理具有共同部分的类型 A 和 B。如果 A 和 B 之间没有交集,或者属性类型互斥,结果可能会得到 never 类型,或者生成的对象中某些属性类型为 never。
交叉类型常用于有交集的类型 A,B。如果A,B无交集可能会得到never,也有可能是属性为never。
以上就是TypeScript 交叉类型使用方法示例总结的详细内容,更多关于TypeScript 交叉类型的资料请关注本站其它相关文章!