WinddSnow

TypeScript-Union-and-Intersection-Types

字数统计: 3k阅读时长: 12 min
2026/07/30

第67课:联合类型与交叉类型——|&、类型收窄、字面量类型

在 TypeScript 中,类型并非总是单一的。现实世界的值往往具有多种可能的形式:一个变量可能是字符串或数字;一个函数的参数可能接受多个不同的接口;一个组合配置需要同时满足多个约束。联合类型(|)和交叉类型(&)是表达这种灵活性的核心工具。本节课将深入两者的语法与语义,重点讲解如何通过类型守卫(Type Guards)将联合类型安全地“缩窄”为具体类型,以及交叉类型在组合对象形状和混入模式中的实战应用。


1. 联合类型(Union Types):A | B

联合类型表示一个值可以是多种类型中的一种。语法为 typeA | typeB,读作“typeA 或 typeB”。

1
2
3
4
5
6
7
8
9
function formatId(id: string | number): string {
if (typeof id === 'string') {
return id.toUpperCase(); // id 被缩窄为 string
}
return id.toString(16); // id 被缩窄为 number
}

console.log(formatId('abc')); // 'ABC'
console.log(formatId(255)); // 'ff'

1.1 联合类型的属性访问限制

在没有缩窄类型之前,只能访问联合类型中所有成员共有的属性。如果访问某个成员独有的属性,TypeScript 会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Bird {
fly(): void;
layEggs(): void;
}

interface Fish {
swim(): void;
layEggs(): void;
}

function move(pet: Bird | Fish) {
pet.layEggs(); // ✅ 两者都有此方法
// pet.fly(); // ❌ Fish 没有 fly 方法
}

1.2 联合类型与字面量类型

字面量类型(Literal Types)将一个值约束为特定的字符串、数字或布尔值。与联合类型结合,可以创建类似“枚举”的效果。

1
2
3
4
5
6
7
8
9
10
type Direction = 'north' | 'south' | 'east' | 'west';
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type HttpStatus = 200 | 301 | 404 | 500;

function move(dir: Direction): void {
console.log(`Moving ${dir}`);
}

move('north'); // ✅
// move('up'); // ❌ Argument of type '"up"' is not assignable

字面量联合类型远比传统的数字枚举更灵活——它不需要导入任何东西,在运行时零开销,并且在条件判断中享有完整的类型缩窄支持。


2. 类型收窄(Type Narrowing)

类型收窄是将联合类型的变量从“可能是多种类型”逐步缩小到“确定是某一种类型”的过程。TypeScript 通过控制流分析自动识别多种收窄模式。

2.1 typeof 类型守卫

适用于 JavaScript 原始类型(stringnumberbooleanbigintsymbolundefinedobjectfunction)。

1
2
3
4
5
6
7
8
9
function process(value: string | number | boolean) {
if (typeof value === 'string') {
console.log(value.toUpperCase()); // value: string
} else if (typeof value === 'number') {
console.log(value.toFixed(2)); // value: number
} else {
console.log(value ? 'true' : 'false'); // value: boolean
}
}

2.2 instanceof 类型守卫

用于检查对象是否为某个类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dog {
bark() { console.log('Woof!'); }
}
class Cat {
meow() { console.log('Meow!'); }
}

function speak(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animal: Dog
} else {
animal.meow(); // animal: Cat
}
}

2.3 in 操作符

检查对象是否包含某个属性,适用于区分不同接口的联合类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Car {
drive(): void;
wheels: number;
}
interface Boat {
sail(): void;
length: number;
}

function operate(vehicle: Car | Boat) {
if ('drive' in vehicle) {
vehicle.drive(); // vehicle: Car
} else {
vehicle.sail(); // vehicle: Boat
}
}

2.4 自定义类型谓词(Type Predicates)

当内置的 typeof/instanceof/in 无法表达复杂逻辑时,可以编写返回值为 arg is Type 的自定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface User {
id: number;
name: string;
email: string;
}

function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}

function processData(data: unknown) {
if (isUser(data)) {
console.log(`User: ${data.name}`); // data: User
} else {
console.log('Invalid data');
}
}

类型谓词 obj is User 告诉 TypeScript:“如果该函数返回 true,则参数 obj 一定是 User 类型”。这是最强大的类型收窄工具。

2.5 switch 与字面量联合类型的穷尽性检查

当联合类型是字面量联合时,switchdefault 分支可以利用 never 类型进行穷尽性检查——确保所有可能的情况都被处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Shape = 'circle' | 'square' | 'triangle';

function getArea(shape: Shape): number {
switch (shape) {
case 'circle': return Math.PI * 1 * 1;
case 'square': return 1 * 1;
case 'triangle': return 0.5 * 1 * 1;
default:
// 如果 Shape 新增了类型但未添加 case,这里会报编译错误
const exhaustive: never = shape;
return exhaustive;
}
}

3. 交叉类型(Intersection Types):A & B

交叉类型将多个类型合并为一个类型,新的类型必须同时满足所有被合并类型的约束。语法为 typeA & typeB,读作“typeA 且 typeB”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Nameable {
name: string;
}
interface Ageable {
age: number;
}

type Person = Nameable & Ageable;

const person: Person = {
name: 'Alice',
age: 25
// 必须同时包含 name 和 age
};

3.1 交叉类型的冲突处理

当合并的类型中同名属性的类型不兼容时,交叉结果可能是 never——表示无法构造满足所有约束的值。

1
2
3
4
5
6
7
8
type A = { value: string };
type B = { value: number };

type Conflicting = A & B;
// Conflicting 的类型为 { value: never }
// 因为 value 必须同时是 string 和 number,而它们的交集是 never

// const x: Conflicting = { value: 'hello' }; // ❌ 类型错误

原则:基本类型的交叉取其交集string & number = neverstring & string = string。对象的交叉取其属性合并,同名属性的类型递归交叉。

3.2 交叉类型用于组合多个约束

交叉类型非常适合描述“某对象必须同时满足多个接口”的场景,例如混入模式(Mixin)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Logger {
log(message: string): void;
}
interface Timestamp {
createdAt: Date;
}

type LoggerWithTimestamp = Logger & Timestamp;

function createLogger(): LoggerWithTimestamp {
return {
log(message) { console.log(message); },
createdAt: new Date()
};
}

3.3 交叉类型用于扩展类型别名

类型别名不能使用 extends,但可以使用交叉类型组合多个类型:

1
2
3
4
5
6
7
8
9
type BaseConfig = { url: string };
type AuthConfig = { token: string };
type FullConfig = BaseConfig & AuthConfig & { timeout: number };

const config: FullConfig = {
url: 'https://api.example.com',
token: 'abc123',
timeout: 5000
};

4. 联合类型与交叉类型的结合

联合和交叉可以组合使用,但需要注意分配律A & (B | C) 等价于 (A & B) | (A & C)

1
2
3
4
5
6
7
8
type Admin = { role: 'admin'; permissions: string[] };
type User = { role: 'user'; name: string };
type Guest = { role: 'guest'; sessionId: string };

// 交叉 + 联合
type AuthUser = Admin | User;
type SessionedUser = AuthUser & { sessionId: string };
// SessionedUser 等价于 (Admin & { sessionId: string }) | (User & { sessionId: string })

5. 联合类型 vs 交叉类型 对比总结

特性 联合类型 A | B 交叉类型 A & B
语义 “要么是 A,要么是 B” “既是 A,又是 B”
属性访问 只能访问共有的属性 可以访问所有属性
适用场景 函数参数可接受多种类型、状态机、变体数据 组合多个接口、混入模式、扩展类型别名
冲突处理 无冲突概念(成员不会同时存在) 同名属性类型不兼容 → never
常见配合 类型守卫(typeof、instanceof、类型谓词) 泛型约束中的多重要求

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 以下哪个 TypeScript 类型守卫用于检查对象的类实例?
    A. typeof
    B. instanceof
    C. in
    D. is

  2. (单选) { name: string } & { age: number } 的结果类型是什么?
    A. { name: string } | { age: number }
    B. { name: string, age: number }
    C. never
    D. { name: string, age: number } | never

  3. (填空) 要定义一个变量只可能是字符串 'left''right',其类型应写作 ______

  4. (多选) 以下哪些方法可以用于缩窄联合类型?
    A. typeof val === 'string'
    B. val instanceof Array
    C. 'property' in val
    D. 类型谓词函数 val is Type

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要编写一个 TypeScript 函数 describeShape(shape),接收一个联合类型的图形对象,返回其描述字符串。要求如下:

  • 定义 CircleRectangleTriangle 三个接口,各有不同的属性(如 radiuswidth/heightbase/height)。
  • 定义类型 Shape = Circle | Rectangle | Triangle
  • describeShape 接收 Shape 类型,使用类型守卫in 或自定义类型谓词)缩窄每种类型,返回描述字符串(包含面积计算)。
  • 使用 default + never 实现穷尽性检查。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 代码。提示词中需明确指定三个接口的结构、类型守卫的方式和穷尽性检查。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建 TypeScript 文件 src/describeShape.ts,实现一个完整的类型收窄示例。要求:

  • 定义 interface Circle { kind: 'circle'; radius: number; }interface Rectangle { kind: 'rectangle'; width: number; height: number; }interface Triangle { kind: 'triangle'; base: number; height: number; }
  • 定义联合类型 type Shape = Circle | Rectangle | Triangle;
  • 实现函数 describeShape(shape: Shape): string,使用 switch (shape.kind) 对每种类型进行收窄,返回包含面积计算的描述字符串(如 'Circle with radius 5 has area 78.54')。
  • default 分支使用 const exhaustive: never = shape; 实现穷尽性检查。
  • 添加 JSDoc 注释。在末尾添加三个调用示例并打印结果。
  • 代码使用严格 TypeScript,完成后输出完整文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请解释 TypeScript 中联合类型和交叉类型的区别。什么是类型收窄?请列举至少三种类型收窄的方式并给出示例。

参考答案

  • **联合类型 A | B**:表示一个值可以是多种类型中的一种。只能安全访问所有成员共有的属性。适用于函数参数可接受多种类型、状态机、字面量枚举等场景。
  • **交叉类型 A & B**:表示一个值必须同时满足所有合并类型的约束。可以访问所有类型的全部属性。适用于组合多个接口、混入模式、扩展类型别名。
  • 类型收窄:将联合类型从“可能多种”缩小到“确定一种”的过程。TypeScript 通过控制流分析自动识别收窄模式。

三种收窄方式:

  1. typeof 守卫if (typeof val === 'string') { /* val 被收窄为 string */ }
  2. instanceof 守卫if (val instanceof Date) { /* val 被收窄为 Date */ }
  3. 自定义类型谓词function isUser(obj: unknown): obj is User { return ...; } —— 返回 true 后 obj 被收窄为 User
  4. in 操作符if ('radius' in shape) { /* shape 被收窄为 Circle */ }
  5. switch + 字面量判别:通过 shape.kindswitch 各分支自动收窄。

课后练习答案

一、概念自测答案

  1. B

    • 解析:instanceof 检查对象是否为某类的实例,用于类实例的类型缩窄。
  2. B

    • 解析:两个对象类型的交叉类型合并所有属性,结果为 { name: string, age: number }
  3. 'left' | 'right'

    • 解析:字面量联合类型,变量只能取这两个字符串之一。
  4. A、B、C、D

    • 解析:四种方式均为 TypeScript 支持的类型缩窄手段。typeof 用于原始类型,instanceof 用于类实例,in 用于属性存在性检查,类型谓词用于自定义逻辑。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请编写 TypeScript 函数 describeShape(shape: Shape): string,实现联合类型的收窄。要求:

  • 定义 Circle{ kind: 'circle', radius: number })、Rectangle{ kind: 'rectangle', width: number, height: number })、Triangle{ kind: 'triangle', base: number, height: number })。
  • type Shape = Circle | Rectangle | Triangle
  • 使用 switch (shape.kind) 分支收窄,计算面积并返回描述字符串。
  • default 分支使用 const _: never = shape 做穷尽性检查。
  • 添加 JSDoc 注释,提供三个调用示例注释。输出完整代码。”
CATALOG
  1. 1. 第67课:联合类型与交叉类型——| 与 &、类型收窄、字面量类型
    1. 1.1. 1. 联合类型(Union Types):A | B
      1. 1.1.1. 1.1 联合类型的属性访问限制
      2. 1.1.2. 1.2 联合类型与字面量类型
    2. 1.2. 2. 类型收窄(Type Narrowing)
      1. 1.2.1. 2.1 typeof 类型守卫
      2. 1.2.2. 2.2 instanceof 类型守卫
      3. 1.2.3. 2.3 in 操作符
      4. 1.2.4. 2.4 自定义类型谓词(Type Predicates)
      5. 1.2.5. 2.5 switch 与字面量联合类型的穷尽性检查
    3. 1.3. 3. 交叉类型(Intersection Types):A & B
      1. 1.3.1. 3.1 交叉类型的冲突处理
      2. 1.3.2. 3.2 交叉类型用于组合多个约束
      3. 1.3.3. 3.3 交叉类型用于扩展类型别名
    4. 1.4. 4. 联合类型与交叉类型的结合
    5. 1.5. 5. 联合类型 vs 交叉类型 对比总结
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)