在上一节中,我们学习了联合类型(|),它代表的是 “或” 的关系,用于使得一个变量可以在多种类型中灵活切换。但在实际的业务开发中,我们往往还会遇到另外一种截然相反的需求。
假设在我们的系统中,原本已经定义好了一个基础的 “用户” 类型(包含姓名和年龄)。现在由于业务扩张,新增了一个 “员工” 的概念(包含员工编号和部门)。如果我们想要定义一个 “正式员工”,他既要有基础用户的属性,也要有员工的属性。
像这种 “既要又要” 的场景,联合类型(|)就帮不上忙了。这个时候,我们需要使用 TypeScript 的另一个强大工具:交叉类型(Intersection Types)。
TypeScript 交叉类型是什么?
在 TypeScript 中,交叉类型是一种用于将多个类型合并为一个包含了所有特征的 “超级类型”。
语法:
type 新类型 = 类型1 & 类型2 & ...;说明:
联合类型使用的是 “|” 符号,类似逻辑 “或”。而交叉类型使用的是 “&” 符号,类似逻辑 “与”。
提示: 交叉类型最常用的场景是:合并对象类型。
示例 1:使用交叉类型
// 定义用户类型
type User = {
name: string;
age: number;
};
// 定义员工类型
type Employee = {
employeeId: string;
department: string;
};
// 合并类型
type Staff = User & Employee;
// 正常赋值:必须同时包含 User 和 Employee 的所有属性
const currentStaff: Staff = {
name: "Jack",
age: 20,
employeeId: "E-1001",
department: "研发部"
};
console.log(currentStaff.name);
console.log(currentStaff.department);运行结果如下。
Jack
研发部分析:
在这个例子中,Staff 类型是 User 和 Employee 的交叉类型。这就意味着,currentStaff 这个对象必须同时拥有这两个类型中的所有属性。
如果漏掉了任何一个属性(比如没写 department),那么 TypeScript 编译器就会报错拦截,并提示缺少对应的属性。
TypeScript 交叉类型的应用
在 TypeScript 中,交叉类型最强大的地方在于:它可以随写随用,不需要为了合并两个结构去专门声明一个新的接口。这在处理第三方库、或者临时组合 API 响应对象时非常高效。
示例 2:动态组合对象
interface BaseResponse {
code: number;
message: string;
}
interface ArticleData {
title: string;
author: string;
}
// 模拟一个请求响应,直接在函数返回值里使用交叉类型
function getArticle(): BaseResponse & ArticleData {
return {
code: 200,
message: "请求成功",
title: "TypeScript 接口",
author: "Jack"
};
}
const responseData = getArticle();
console.log(responseData.title);运行结果如下。
TypeScript 接口分析:
在实际项目开发(例如封装 Axios 请求)中,我们经常会有一个通用的外层响应结构(如 BaseResponse),而具体的业务数据结构每次都不同。使用交叉类型 “&”,我们可以优雅且轻量地将通用结构和业务结构 “粘” 在一起,而不需要为每一个接口都额外写一个长篇大论的继承关系。
TypeScript 交叉类型的注意事项
虽然交叉类型用来合并 “对象类型” 时非常爽,但如果你试图去交叉两个 “基础数据类型”,那么 TypeScript 就会给你一个巨大的 “惊喜”。
// 试图把字符串和数字交叉起来
type Impossible = string & number;小伙伴们思考一下,这段代码中 Impossible 到底是什么类型?一个值可能 “既是数字、同时又是字符串” 吗?很显然,这种值是绝对不可能存在的。
因此,TypeScript 非常聪明地推断出,这种类型是永远不可能有对应的值的。如果我们在 VS Code 中把鼠标悬浮在 Impossible 上,你会发现它的最终类型变成了我们在前面刚学过的——never(永远不会到达的终点、永远不存在的值),如下图所示。

示例 3:同名属性的冲突
interface TypeA {
id: number;
name: string;
}
interface TypeB {
id: string; // 注意这里:同名属性 id,但类型变成了 string
age: number;
}
type SuperType = TypeA & TypeB;
// 错误操作:无论是赋数字还是字符串,都会报错
// const obj: SuperType = {
// id: 1,
// name: "Jack",
// age: 20
// };
// 报错:不能将类型“number”分配给类型“never”。分析:
小伙伴们仔细看,TypeA 要求 id 是数字,而 TypeB 要求 id 是字符串。当它们被合并成 SuperType 时,TypeScript 会尝试把 id 这个属性也交叉起来,也就是 id: number & string。
根据我们刚刚学过的知识,number & string 的结果是 never!这就导致合并出来的 SuperType 中的 id 变成了 never 类型,我们永远无法给它赋任何合法的值,整个对象类型也就彻底报废了。
在实际开发中,遇到需要组合复杂类型的情况,一定要注意规避这种同名属性类型冲突的低级错误。
企业级实战 —— 如何解决同名冲突?
有小伙伴可能会问:“如果在真实的业务中,有时别无选择,必须把 TypeA 和 TypeB 合并,并且我希望以 TypeB 的 id(字符串)为准,此时应该怎么办呢?”
在企业级开发中,直接交叉会产生 never,因此我们通常会结合 TypeScript 的内置工具类型 Omit(剔除属性)来曲线救国:
// 先把 TypeA 里面的 "id" 属性挖掉,然后再和 TypeB 进行交叉合并
type SuperType = Omit<TypeA, "id"> & TypeB;
const obj: SuperType = {
id: "ID-1001", // 此时 id 安全地变成了 string 类型
name: "Jack",
age: 20
};这样就完美解决了同名属性的冲突问题!关于 Omit 以及更多像魔法一样的内置工具类型,我们会在后续的 “TypeScript 内置泛型工具类型” 一节中详细介绍。
最后,对于联合类型和交叉类型,我们来总结一下:
- 当我们想要表示 “或者” 时,应该使用联合类型(|)。
- 当我们想要表示 “既要又要” 时,应该使用交叉类型(&)。
