第 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 | 字面量联合类型,编译后无运行时开销 |
动手练习
- 类型收窄:写一个函数
format(value: string | number | boolean): string,分别处理三种类型 - 可辨识联合:定义一个
Result<T>类型,包含success和error两种状态 - 字面量:用字面量类型定义一个
HttpStatus类型,覆盖 200/301/404/500
推荐阅读
- 📖 TypeScript Handbook - Narrowing — 收窄机制详解
- 📖 TypeScript Handbook - Literal Types — 字面量类型
- 🌐 Discriminated Unions 模式 — 官方推荐模式
下一章:第 96 章:对象类型与类型别名 →