Skip to content
第 95 / 250 章前端⏱ 10 分钟阅读

第 95 章:联合类型与字面量类型

学习目标

  • 掌握联合类型(Union Types)的使用
  • 理解类型收窄(Narrowing)的概念
  • 学会字面量类型(Literal Types)的妙用
  • 能用联合类型替代枚举

一、联合类型(Union Types)

1.1 什么是联合类型

变量可以是多种类型之一

typescript
let value: string | number;

value = 'Tom';    // ✅
value = 123;      // ✅
value = true;     // ❌ Type 'boolean' is not assignable to type 'string | number'

1.2 函数参数

typescript
// 接受多种类型
function printId(id: string | number) {
  console.log(`Your ID is: ${id}`);
}

printId(101);      // ✅
printId('ABC');    // ✅
printId(true);     // ❌

1.3 联合类型的"限制"

typescript
function printId(id: string | number) {
  console.log(id.toUpperCase());  // ❌ Property 'toUpperCase' does not exist on type 'number'
  console.log(id.toFixed(2));     // ❌ Property 'toFixed' does not exist on type 'string'
}

原因:联合类型只能访问所有组成类型的公共方法

二、类型收窄(Narrowing)

通过条件判断,TS 会自动把宽类型收窄到具体类型。

2.1 typeof 收窄

typescript
function printId(id: string | number) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase());  // ✅ 这里 id 是 string
  } else {
    console.log(id.toFixed(2));     // ✅ 这里 id 是 number
  }
}

2.2 真值收窄

typescript
function printName(name: string | null | undefined) {
  if (name) {
    console.log(name.toUpperCase());  // ✅ 收窄为 string
  }
}

2.3 in 操作符收窄

typescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim();  // ✅ 收窄为 Fish
  } else {
    animal.fly();   // ✅ 收窄为 Bird
  }
}

2.4 instanceof 收窄

typescript
function logDate(date: string | Date) {
  if (date instanceof Date) {
    console.log(date.toISOString());  // ✅ 收窄为 Date
  } else {
    console.log(date.length);         // ✅ 收窄为 string
  }
}

2.5 自定义类型守卫(Type Guard)

typescript
interface User {
  kind: 'user';
  name: string;
}

interface Admin {
  kind: 'admin';
  permissions: string[];
}

function isAdmin(person: User | Admin): person is Admin {
  return person.kind === 'admin';
}

function check(person: User | Admin) {
  if (isAdmin(person)) {
    console.log(person.permissions);  // ✅ 收窄为 Admin
  }
}

三、字面量类型(Literal Types)

3.1 什么是字面量类型

typescript
// 字面量类型:值本身就是类型
let direction: 'up' | 'down' | 'left' | 'right';
direction = 'up';      // ✅
direction = 'forward'; // ❌ Type '"forward"' is not assignable to type '"up" | "down" | "left" | "right"'

3.2 数字字面量

typescript
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 3;   // ✅
dice = 7;   // ❌

3.3 布尔字面量

typescript
let alwaysTrue: true;
alwaysTrue = true;   // ✅
alwaysTrue = false;  // ❌

3.4 联合 + 字面量 = 枚举替代

typescript
// ❌ 用 enum 定义(TS 5.x 之前流行)
enum OrderStatus {
  Pending = 'PENDING',
  Paid = 'PAID',
  Shipped = 'SHIPPED'
}

// ✅ 用联合 + 字符串字面量(TS 5.x 推荐)
type OrderStatus = 'pending' | 'paid' | 'shipped';

function updateOrder(status: OrderStatus) {
  console.log(`订单状态:${status}`);
}

updateOrder('pending');  // ✅
updateOrder('cancel');   // ❌

为什么推荐字面量联合? 编译后完全消失,运行时无开销;和 React/Vue 配合更自然。

四、可辨识联合(Discriminated Unions)

联合 + 字面量 + 公共字段 = 类型收窄利器

4.1 经典案例:状态机

typescript
// ① 定义联合类型,每个成员有 kind 字段
type State =
  | { kind: 'idle' }
  | { kind: 'loading' }
  | { kind: 'success'; data: User[] }
  | { kind: 'error'; error: Error };

// ② 通过 kind 收窄
function render(state: State) {
  switch (state.kind) {
    case 'idle':
      return '加载中';
    case 'loading':
      return '正在加载...';
    case 'success':
      return state.data.map(u => u.name).join(',');  // ✅ 自动推断有 data
    case 'error':
      return state.error.message;  // ✅ 自动推断有 error
  }
}

4.2 配合穷尽性检查

typescript
type Direction = 'up' | 'down' | 'left' | 'right';

function handleDirection(d: Direction) {
  switch (d) {
    case 'up':    return '↑';
    case 'down':  return '↓';
    case 'left':  return '←';
    case 'right': return '→';
    default:
      const _exhaustive: never = d;  // 漏掉任何 case,这里会报错
      return _exhaustive;
  }
}

加新方向时,TS 会强制提醒你处理。

五、对象字面量与联合类型

5.1 自动收窄

typescript
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function area(shape: Shape): number {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius ** 2;  // ✅ 推断有 radius
  }
  return shape.side ** 2;                // ✅ 推断有 side
}

5.2 复杂场景

typescript
type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keydown'; key: string; code: number }
  | { type: 'submit'; formData: Record<string, string> };

function handle(event: Event) {
  switch (event.type) {
    case 'click':
      console.log(`点击坐标:${event.x}, ${event.y}`);
      break;
    case 'keydown':
      console.log(`按键:${event.key} (code=${event.code})`);
      break;
    case 'submit':
      console.log(`表单数据:`, event.formData);
      break;
  }
}

六、联合类型 vs 类型别名

typescript
// type alias:给类型起名字
type Status = 'success' | 'error' | 'pending';

// 接口:定义对象结构
interface ApiResponse {
  code: number;
  message: string;
  data: unknown;
}

// 组合
type StatusResponse = ApiResponse & { status: Status };
场景推荐
联合类型type
字面量类型type
对象结构interface
函数类型type
自动合并interface

七、判别式字段命名约定

typescript
// ✅ 推荐用 kind / type / variant 之一,保持一致
type UserAction =
  | { kind: 'login'; username: string }
  | { kind: 'logout' }
  | { kind: 'update'; data: User };

type MouseEvent =
  | { type: 'click'; x: number; y: number }
  | { type: 'move'; x: number; y: number };

type ApiState =
  | { variant: 'loading' }
  | { variant: 'success'; data: any }
  | { variant: 'error'; error: Error };

八、实战案例:表单校验

typescript
// 表单字段验证结果
type ValidationResult =
  | { valid: true; value: string }
  | { valid: false; error: string };

function validateEmail(input: string): ValidationResult {
  if (!input.includes('@')) {
    return { valid: false, error: '邮箱格式不正确' };
  }
  return { valid: true, value: input };
}

function submit(result: ValidationResult) {
  if (result.valid) {
    console.log('提交:', result.value);  // ✅ TS 知道有 value
  } else {
    console.error('错误:', result.error);  // ✅ TS 知道有 error
  }
}

九、常见错误

9.1 联合类型赋值给单一类型

typescript
let value: string | number = 'Tom';
let str: string = value;  // ❌ Type 'string | number' is not assignable to type 'string'

修复

typescript
// 1. 类型断言(如果你确定)
let str = value as string;

// 2. 收窄后赋值
if (typeof value === 'string') {
  let str: string = value;  // ✅
}

9.2 联合类型访问特有方法

typescript
function padLeft(value: string, padding: string | number) {
  if (typeof padding === 'number') {
    return ' '.repeat(padding) + value;
  }
  return padding + value;
}

十、本章小结

要点关键
联合类型A | B | C,变量可以是多种类型之一
类型收窄通过 typeof / in / instanceof / 自定义守卫收窄
字面量类型type Status = 'pending' | 'paid'
可辨识联合联合 + 共同字段 + switch 收窄
推荐替代 enum字面量联合类型,编译后无运行时开销

动手练习

  1. 类型收窄:写一个函数 format(value: string | number | boolean): string,分别处理三种类型
  2. 可辨识联合:定义一个 Result<T> 类型,包含 successerror 两种状态
  3. 字面量:用字面量类型定义一个 HttpStatus 类型,覆盖 200/301/404/500

推荐阅读


下一章第 96 章:对象类型与类型别名

本站基于 VitePress 构建 · 由 Codebook 团队维护