TypeScript 联合类型深入解析

联合类型(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是交叉类型,必须同时满足CircleSquare,那么kind对应的属性值的类型就要交叉,'circle' & 'square'结果是never,因此不可能存在满足Cross类型的数据,Cross被推导(运算)成了neverkeyof Cross等同于keyof neverkeyof 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.

对象的联合类型,只要满足一个即可,未进行收窄时,能访问公共属性,并且属性值的类型是所有联合类型对应属性值的类型的联合。对象的交叉类型,必须满足所有条件,能访问全部数据,并且属性值的类型是所有交叉类型对应属性值的类型的交叉。

总结

联合类型的特点,都是为了遵守类型安全的原则,可以归纳为以下几条关键要点:

要点 说明
定义 联合类型表示"满足其一即可",用 `
来源 除了手动声明,keyofextendsin、模板字符串等运算符进行类型运算产生
访问规则 未收窄时,只能访问公共属性,属性值类型是各成员的"联合"
收窄方式 可辨识联合(switch + kind 标签)、 in 运算符、自定义类型守卫等
函数同名属性 参数取交集&),返回值取联合 (`
获取全部键 T extends any ? keyof T : never 可以取出非公共键
vs 交叉类型 联合是"或"(满足其一),交叉是"且"(全部满足);keyof 联合取公共键,keyof 交叉取全部键
相关推荐
叫我少年3 小时前
TypeScript 基础类型
typescript
大爱编程♡6 小时前
Vue3+TypeScript+element-plus的文章管理项目(配后端接口)-退出及文章管理页面
前端·javascript·typescript
如此风景10 小时前
TS装饰器详解
typescript
人间凡尔赛21 小时前
2026 年 React Server Components 完全指南:Next.js 16 全栈开发最佳实践
前端·typescript·react·全栈·next.js
晓说前端1 天前
TypeScript 核心语法进阶 —— 字面量类型与类型推论
前端·typescript
布兰妮甜1 天前
从 0 搭建企业级 Vue3/Vite 脚手架(规范、eslint、husky、打包、环境变量全流程)
typescript·vue3·vite·脚手架·前端工程化
东方小月1 天前
从0开发一个 Coding Agent(一):前言
前端·人工智能·typescript
gis开发之家2 天前
《Vue3 从入门到大神35篇》Vue3 源码详解(五):effect 依赖收集原理——track 与 trigger 是如何工作的?
javascript·typescript·前端框架·vue3·vue3源码
退休倒计时2 天前
【每日一题】LeetCode 131. 分割回文串 TypeScript
算法·leetcode·typescript