TypeScript 交叉类型的实现
作者:小赵同学WoW
摘要
本文介绍 TypeScript 交叉类型 & 的含义与使用方式,说明对象属性组合、同名属性冲突与 never 的形成,并对比联合类型,梳理类型扩展和职责组合等常见场景。
1. 什么是交叉类型
交叉类型使用 & 连接多个类型,表示一个值需要同时满足所有类型。
type A = { a: string };
type B = { b: number };
type C = A & B;
C 类型的值既要满足 A,也要满足 B:
const value: C = {
a: 'hello',
b: 100,
}; // ✅
可以把 A & B 读成:
既是 A,又是 B。
2. 对象交叉
交叉类型最常见的用途,是组合多个对象类型。
type Student = {
name: string;
score: number;
};
type Teacher = {
name: string;
age: number;
subject: string;
};
type User = Student & Teacher;
User 必须拥有两边要求的全部属性:
const user: User = {
name: '小王',
score: 95,
age: 30,
subject: '数学',
}; // ✅
少了任何必选属性都不可以:
const user: User = {
name: '小王',
age: 30,
subject: '数学',
}; // ❌ 缺少 Student 要求的 score
一句话记忆:
对象交叉不是二选一,而是把两边的要求都加到同一个值上。
3. 为什么叫“交叉”,结果却像属性合并
刚开始学习时,很容易认为两个对象的“交集”应该只留下公共属性。这是把属性集合和类型的值集合混在了一起。
type A = { name: string; score: number };
type B = { name: string; age: number };
type C = A & B;
A & B 表示:寻找既能赋值给 A,又能赋值给 B 的值。
要同时满足这两个类型,对象就必须拥有:
- A 要求的 name、score。
- B 要求的 name、age。
所以结果看起来像属性合并:
const person: C = {
name: '小明',
score: 90,
age: 18,
};
这里取的是“同时属于 A 类型和 B 类型的值”,不是取“A 和 B 的公共属性”。
4. 同名属性如何处理
4.1 同名属性类型相同
如果两边存在同名属性,并且属性类型相同,那么该属性正常合并:
type A = {
name: string;
score: number;
};
type B = {
name: string;
age: number;
};
type C = A & B;
name 同时满足两边的 string,因此仍然是 string。
const person: C = {
name: '小明',
score: 90,
age: 18,
}; // ✅
4.2 同名属性类型有重合
同名属性本身也会进行交叉:
type A = {
id: string;
};
type B = {
id: string | number;
};
type C = A & B;
id 既要是 string,又要是 string | number,所以最终只能是 string:
const value: C = {
id: 'abc',
}; // ✅
4.3 同名属性类型完全冲突
如果同名属性的类型完全没有重合,属性会变成 never:
type A = {
id: string;
};
type B = {
id: number;
};
type C = A & B;
此时 C['id'] 相当于:
type Id = string & number; // never
不存在一个普通值能同时是 string 和 number,因此无法正常创建 C 类型的对象:
const value: C = {
id: 'abc', // ❌ string 不能赋值给 never
};
5. 基础类型的交叉
交叉类型不只是简单地把内容放在一起,而是寻找同时满足两边条件的值。
没有重合部分
type Result = string & number; // never
一个值不可能既是字符串又是数字,所以结果是 never。
type Color = 'red' & 'blue'; // never
一个值也不可能同时是 'red' 和 'blue'。
有重合部分
type Result = (string | number) & string;
string | number 和 string 都允许的值只有字符串,所以结果是:
type Result = string;
再看一个字面量例子:
type Color = ('red' | 'blue') & 'red';
// 结果是 'red'
6. 交叉类型和联合类型的区别
type A = {
name: string;
};
type B = {
age: number;
};
联合类型A | B
表示满足其中至少一个类型即可:
const value1: A | B = { name: '小明' }; // ✅
const value2: A | B = { age: 18 }; // ✅
const value3: A | B = { name: '小明', age: 18 }; // ✅
交叉类型A & B
表示必须同时满足两个类型:
const value1: A & B = { name: '小明' }; // ❌ 缺少 age
const value2: A & B = { age: 18 }; // ❌ 缺少 name
const value3: A & B = { name: '小明', age: 18 }; // ✅
| 类型 | 含义 | 赋值要求 | 使用属性 | |
|---|---|---|---|---|
| `A | B` | A 或 B | 至少完整满足一个 | 未收窄时只能使用公共属性 |
| A & B | A 且 B | 同时完整满足所有类型 | 可以使用两边的属性 |
记忆:
- |:选择范围变大,满足其中一种即可。
- &:要求变多,必须同时满足所有条件。
7. 实际用途
7.1 在已有类型上增加属性
type Goods = {
id: number;
name: string;
price: number;
};
type CartItem = Goods & {
count: number;
};
type OrderItem = Goods & {
count: number;
totalPrice: number;
};
创建购物车商品时,既要提供商品信息,也要提供数量:
const cartItem: CartItem = {
id: 1,
name: '苹果',
price: 5,
count: 2,
};
创建订单商品时,需要满足更多要求:
const orderItem: OrderItem = {
id: 1,
name: '苹果',
price: 5,
count: 2,
totalPrice: 10,
};
这种写法可以复用已有类型,达到类似“扩展类型”的效果。
7.2 组合不同职责的数据
type User = {
id: number;
name: string;
};
type Timestamp = {
createdAt: Date;
updatedAt: Date;
};
type UserRecord = User & Timestamp;
UserRecord 同时包含用户信息和时间信息:
const user: UserRecord = {
id: 1,
name: '小明',
createdAt: new Date(),
updatedAt: new Date(),
};
8. 结构类型带来的赋值现象
TypeScript 主要根据对象的结构判断能否赋值。属性更多的对象,可以赋值给只要求其中一部分属性的类型。
const cartItem: CartItem = {
id: 1,
name: '苹果',
price: 5,
count: 2,
};
const goods: Goods = cartItem; // ✅ cartItem 拥有 Goods 要求的全部属性
反过来不可以:
const goods: Goods = {
id: 1,
name: '苹果',
price: 5,
};
const cartItem: CartItem = goods; // ❌ goods 缺少 count
判断时不要只看类型名称,要看这个值是否具备目标类型要求的全部结构。
9. 总结
- 交叉类型使用 &,表示一个值必须同时满足所有类型。
- 对象交叉后,需要提供各个对象类型要求的全部必选属性。
- 同名属性也要同时满足两边的类型要求。
- 同名属性完全冲突时,该属性会变成 never,通常导致对象无法创建。
- A | B 是满足至少一个,A & B 是同时满足全部。
- 交叉类型常用于复用已有对象类型,并在其基础上增加新属性。
最重要的一句话:
联合类型是在多个可能中选一个;交叉类型是把多个要求同时加到一个值上。
到此这篇关于TypeScript 交叉类型的实现的文章就介绍到这了,更多相关TypeScript 交叉类型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
