联合类型(union types)是指多个类型组成的一个新类型,使用符号 | 表示。
可以通过声明或类型运算得到一个联合类型。
声明联合类型
ts
type U = string|number;
let x: U = 123
x = 'abc'
单独创建很简单,将多个类型组合,就可以得到一个联合类型。
类型运算的结果是一个联合类型
keyof 和 \[\]
ts
const StatusEnum = {
/** 默认 */
DEFAULT: 0,
/** 加载中 */
LOADING: 1,
/** 成功 */
SUCCESS: 2,
/** 失败 */
FAILED: 3
} as const
上面代码中,定义一个 StatusEnum 常量。
当需要为后端返回数据中的status添加类型时,
ts
type StatusEnumType = typeof StatusEnum
// "DEFAULT" | "LOADING" | "SUCCESS" | "FAILED"
type StatusKeys = keyof StatusEnumType
// 0 | 1 | 2 | 3
type Status = StatusEnumType[StatusKeys]
interface Data {
id: number,
name: string,
status: Status
}
const data: Data = {
id: 1,
name: '联合类型',
status: 2
}
keyof 运算符接受一个对象类型作为参数,返回该对象的所有属性组成的联合类型。 StatusKeys就是StatusEnum所有(自定义)属性的联合类型。
通过[]运算符,就可以很轻松的得到属性值的联合类型。 方括号里面是属性名的联合类型,因此返回的是对应的属性值的联合类型。
extends 条件运算符
当一个裸类型参数(naked type parameter)出现在条件类型的 extends 左侧时,并且是联合类型时,TypeScript 会自动分发(Distributive Conditional Types),对每个成员单独进行条件判断,将结果重新组合,最终得到的还是一个联合类型(可能会自动整合)。
ts
type ToArray<T> = T extends keyof any ? T[] : never
type ArrUnion = ToArray<string | number> // string[] | number[]
in 运算符
TypeScript 语言的类型运算中,in运算符主要用来取出(遍历)联合类型的每一个成员类型。
实现一下内置类型工具Omit
ts
// 指定删除的键名 Keys 可以是对象类型 Type 中不存在的属性,但必须兼容 string|number|symbol
type U = keyof any // string|number|symbol
type Omit<T, K extends U> = {
[P in keyof T as P extends K ? never : P ]: T[P]
}
keyof T拿到泛型T属性名的联合类型,in取出联合类型的每一个成员,再进行条件运算返回重映射的结果,如果结果是never,表示这个结果可以舍弃,仅保留满足条件的属性,从而实现删除属性的目的。
模板字符串
模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串也可以展开联合类型。
ts
type T = 'A'|'B';
type U = '1'|'2';
// 'A1'|'A2'|'B1'|'B2'
type V = `${T}${U}`;
对象的联合类型
在 TS 中,对象的联合类型也极为常见。
ts
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLength: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
type Shape = Circle | Square | Rectangle
只能访问公共属性
联合类型 Shape 只知道是三种类型之一,但并不能确定,为了类型安全,因此只允许访问每个成员都有的属性:
ts
// 获取公共属性
type ShapeKeys = keyof Shape // kind
function getKind(shape: Shape) {
shape.kind // ✅ "circle" | "square" | "rectangle" ------ 公共属性,可以访问
return shape.kind
}
function getArea(shape: Shape) {
shape.radius // ❌ 类型"Shape"上不存在属性"radius"
shape.sideLength // ❌ 不存在
shape.width // ❌ 不存在
}
如果硬要访问非公共属性,必须先收窄类型。
类型收窄的四种方式
1. 添加标签
kind 字段天然就是"标签",用 switch 收窄:
ts
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// shape 被收窄为 Circle
return Math.PI * shape.radius ** 2
case 'square':
// shape 被收窄为 Square
return shape.sideLength ** 2
case 'rectangle':
// shape 被收窄为 Rectangle
return shape.width * shape.height
default:
// shape 在这里是 never ------ 如果新增了形状但没加 case,这里会报错
const _exhaustive: never = shape
return _exhaustive
}
}
area({ kind: 'circle', radius: 10 }) // 314.159...
area({ kind: 'square', sideLength: 5 }) // 25
area({ kind: 'rectangle', width: 3, height: 4 }) // 12
给 switch 加上 default 分支,TypeScript 会用 never 检查是否遗漏了成员------这就是穷举检查(Exhaustiveness Check):
2. in 运算符
检查某个独有属性是否存在:
ts
function area(shape: Shape): number {
if ('radius' in shape) {
return Math.PI * shape.radius ** 2 // Circle
} else if ('sideLength' in shape) {
return shape.sideLength ** 2 // Square
} else {
// 排除了前两种,必然是 Rectangle
return shape.width * shape.height
}
}
3. 自定义类型守卫
用 x is T 返回值类型编写守卫函数:
ts
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle'
}
function area(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2 // ✅ Circle
}
// 剩余是 Square | Rectangle
// ...
}
如何获取全部属性
要想从联合类型中取出所有可能的键,可以利用分布式条件类型:
ts
type AllKeys<T> = T extends any ? keyof T : never
type AllShapeKeys = AllKeys<Shape> // "kind" | "radius" | "sideLength" | "width" | "height"
进一步,拿到某个键的属性值的类型:
ts
// kind 类型
type K = Shape['kind'] // "circle" | "square" | "rectangle"
type PickUnion<T, K extends AllKeys<T>> = T extends { [P in K]: infer V } ? V : never
type Radius = PickUnion<Shape, 'radius'> // number
type Kind = PickUnion<Shape, 'radius'> // "circle" | "square" | "rectangle"
对应公共属性想要获取属性值的类型,直接使用[]运算符,如果不是公共属性,可以使用上面的方法获取。
相同属性的类型
从联合类型上访问同名属性时,TypeScript 会对每个成员分别取值再联合。
上面获取公共属性kind的属性值的类型,就是所有kind的属性值的类型的联合。
ts
type Kind = Shape['kind']
// Circle['kind'] | Square['kind'] | Rectangle['kind'] => "circle" | "square" | "rectangle"
同名函数属性
当同名属性是函数类型 时,有点区别。现在给三种类型都添加draw 属性
ts
interface Circle {
kind: 'circle'
radius: number
draw: (ctx: 'canvas') => void
}
interface Square {
kind: 'square'
sideLength: number
draw: (ctx: 'svg') => void
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
draw: () => Error
}
type Shape = Circle | Square | Rectangle
// ((ctx: "canvas") => void) | ((ctx: "svg") => void) | (() => Error)
type Draw = Shape['draw']
function onDraw(shape: Shape){
shape.draw() // ❌ Expected 1 arguments, but got 0.
}
调用 shape.draw()时,TypeScript 必须确保调用对所有签名都安全。规则是:
| 角色 | 规则 | 原因 |
|---|---|---|
| 参数类型 | 取交集 & |
参数必须同时满足所有重载签名 |
| 返回值类型 | 取联合 ` | ` |
ts
declare const shape: Shape
shape.draw('canvas') // ❌ 参数交集 'canvas' & 'svg' = never,无法传入
参数走交集,两个互斥结果是 never,所以这个函数在调用前,必须先收窄类型:
ts
if (shape.kind === 'circle') {
shape.draw('canvas') // ✅ Circle.draw,参数是 'canvas'
}
与对象的交叉类型比较
联合类型 | 和交叉类型 & 是 TypeScript 中一对互补的概念,对比有什么不同。
ts
interface Circle {
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLength: number
}
// 联合类型:满足其中一个即可
// Circle 或 Square
type Union = Circle | Square
// 获取到的是公共属性
type UnionKeys = keyof Union // kind | draw
// 交叉类型:必须同时满足所有成员
type Cross = Circle & Square
// 获取到的是全部属性
type CrossKeys = keyof Cross // string | number | symbol
CrossKeys应该拿到的是全部属性,可为什么结果是string | number | symbol呢?这是因为Cross是交叉类型,必须同时满足Circle与Square,那么kind对应的属性值的类型就要交叉,'circle' & 'square'结果是never,因此不可能存在满足Cross类型的数据,Cross被推导(运算)成了never。keyof Cross等同于keyof never、keyof any,自然是string | number | symbol。
验证一下:
ts
// Type 'number' is not assignable to type 'never'.
// The intersection 'Cross' was reduced to 'never' because property 'kind' has conflicting types in some constituents.
const crossObj: Cross = 1
number类型不能赋值给never类型,Cross被简化(推导)为never.
对象的联合类型,只要满足一个即可,未进行收窄时,能访问公共属性,并且属性值的类型是所有联合类型对应属性值的类型的联合。对象的交叉类型,必须满足所有条件,能访问全部数据,并且属性值的类型是所有交叉类型对应属性值的类型的交叉。
总结
联合类型的特点,都是为了遵守类型安全的原则,可以归纳为以下几条关键要点:
| 要点 | 说明 |
|---|---|
| 定义 | 联合类型表示"满足其一即可",用 ` |
| 来源 | 除了手动声明,keyof、extends、in、模板字符串等运算符进行类型运算产生 |
| 访问规则 | 未收窄时,只能访问公共属性,属性值类型是各成员的"联合" |
| 收窄方式 | 可辨识联合(switch + kind 标签)、 in 运算符、自定义类型守卫等 |
| 函数同名属性 | 参数取交集 (&),返回值取联合 (` |
| 获取全部键 | T extends any ? keyof T : never 可以取出非公共键 |
| vs 交叉类型 | 联合是"或"(满足其一),交叉是"且"(全部满足);keyof 联合取公共键,keyof 交叉取全部键 |