在 TypeScript 的类型系统中,联合类型 (|) 给了我们“或”的选择,允许一个值成为多种类型之一。而与之相对的,则是提供了“与”能力的交叉类型 (Intersection Types)。它允许我们将多个类型合并(或“交叉”)成一个单一的、拥有所有类型成员的新类型。
交叉类型是实现“组合优于继承”设计原则的强大工具,特别适合用于创建复杂的、由多个部分组合而成的类型。
什么是交叉类型?
交叉类型使用 & 运算符,将多个已存在的类型组合在一起。最终生成的类型将包含所有原始类型的所有属性。
其基本语法如下:
type TypeA = { a: string };
type TypeB = { b: number };
type CombinedType = TypeA & TypeB;
// CombinedType 现在等价于:
// {
// a: string;
// b: number;
// }任何被声明为 CombinedType 的对象,都必须同时拥有 a 和 b 两个属性,并符合其各自的类型要求。
让我们看一个更实际的例子。假设我们正在为一个 CRM 系统建模,一个客户的完整资料由“联系信息”和“业务信息”两部分组成:
type ContactInfo = {
email: string;
phone: string;
};
type BusinessInfo = {
companyName: string;
vatNumber: string;
};
// 使用交叉类型组合成一个完整的客户档案
type CustomerProfile = ContactInfo & BusinessInfo;
const customer: CustomerProfile = {
email: "contact@example.com",
phone: "123-456-7890",
companyName: "Example Inc.",
vatNumber: "VAT123",
};通过 & 运算符,CustomerProfile 类型现在同时拥有了 ContactInfo 和 BusinessInfo 的所有属性。这种方式清晰地表达了“一个完整的客户档案是由联系信息和业务信息共同组成的”这一业务逻辑。
当原始类型相交时
一个有趣且重要的知识点是:当原始类型(如 string, number)进行交叉时会发生什么?
type ImpossibleType = string & number;ImpossibleType 的类型是 never。这是因为在类型系统中,一个值不可能同时是 string 且是 number。never 类型正是用来表示这种永远不会发生的、不合逻辑的类型状态。这个特性在进行类型推导和确保类型安全时非常有用。
交叉类型 (&) vs. 接口扩展 (extends)
初学者可能会对 & 和 extends 感到困惑,因为它们都能实现类型的扩展。但它们的语义和适用场景有着本质的区别。
| 特性 | interface extends ... (接口扩展) | type ... & ... (交叉类型) |
|---|---|---|
| 语义 | 继承 (Inheritance)。表达一种 “is-a” (是一个) 的层次关系。例如,Admin extends User 意味着 Admin 是一种特殊的 User。 | 组合 (Composition)。表达一种 “has-a” (拥有) 的聚合关系,将多个独立的类型特征组合在一起。 |
| 使用场景 | 构建清晰的对象层次结构,如 Square extends Shape。 | 将不同维度的功能“混入”(Mixin)到一个类型中,如 Draggable & Clickable。 |
| 来源 | 只能用于 interface 之间。 | 可以用于 type 或 interface 的任意组合。 |
简单来说:
- 当你能说 “A 是一种 B” 时,使用
extends。 - 当你需要将多个独立的功能块组合成一个新的整体时,使用
&。
实践应用:实现混入 (Mixin) 模式
交叉类型最强大的应用场景之一是实现混入 (Mixin) 模式。Mixin 是一种在不使用类继承的情况下,为对象添加可复用功能的设计模式。
假设我们有两个函数,它们各自为对象添加一种能力:一个添加时间戳,另一个添加日志功能。
type HasTimestamp = {
timestamp: number;
};
type HasLogger = {
log(message: string): void;
};
// 这个函数为一个对象添加时间戳能力
function withTimestamp<T>(obj: T): T & HasTimestamp {
return { ...obj, timestamp: Date.now() };
}
// 这个函数为一个对象添加日志能力
function withLogger<T>(obj: T): T & HasLogger {
return {
...obj,
log: (message: string) => console.log(`[LOG] ${message}`),
};
}
// 基础对象
const basicObject = {
name: "My Component",
};
// 使用函数组合和交叉类型来构建一个功能丰富的对象
const composedObject = withLogger(withTimestamp(basicObject));
// 现在 composedObject 的类型是 { name: string } & HasTimestamp & HasLogger
console.log(composedObject.name);
console.log(composedObject.timestamp);
composedObject.log("Component initialized.");在这个例子中,composedObject 的最终类型是 basicObject 的原始类型与 HasTimestamp 和 HasLogger 的交叉。TypeScript 能够完美地理解这个组合后的新类型,并为我们提供完整的类型检查和代码提示。这是一种极其灵活且类型安全的组合方式。
总结
交叉类型是 TypeScript 中实现“组合”思想的核心工具。它使我们能够以一种声明式的方式,将多个独立的类型合并成一个功能更强大的复合类型。
- 它使用
&运算符,将所有成员类型的属性聚合在一起。 - 它与
extends的继承关系不同,更侧重于功能的组合与聚合。 - 在实现混入 (Mixin) 等高级设计模式时,交叉类型是不可或缺的强大工具。
当你需要构建一个由多个独立部分“拼装”而成的复杂类型时,请记住交叉类型。它将帮助你创建出结构清晰、精确且富有表现力的类型定义。
