第5章 联合类型、交叉类型与可辨识联合

第 5 章 联合类型、交叉类型与可辨识联合

5.1 联合类型(Union Types)

联合类型是TypeScript中表示"可以是多种类型之一"的类型。它就像一把钥匙能开多把锁——一个变量可以是几种类型中的任意一种。


5.1.1 联合类型的声明:string | number

1
2
3
4
5
6
7
8
// 声明联合类型
let value: string | number = "hello";
console.log(value); // hello

value = 42;
console.log(value); // 42

// value = true; // 错误!boolean不在允许的范围内

5.1.2 联合类型的值域概念

5.1.2.1 联合类型的值域 = 各成员类型值域的并集;值可以是任意一个成员类型的值,但不能同时是两个

1
2
3
4
5
6
7
8
9
// string | number的值域 = {所有字符串} ∪ {所有数字}
let mixed: string | number;

mixed = "hello";    // OK
mixed = 123;        // OK
mixed = true;       // 错误!

// 不能同时是两种类型
mixed = "hello" + 123; // 这个表达式的结果是"hello123"(字符串),不是同时有两个类型

5.1.2.2 联合类型的宽度随成员数量增加而扩大,类型收窄(narrowing)可以逐步缩小为具体成员

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 成员越多,类型越宽
type Narrow = "A";
type Medium = "A" | "B";
type Wide = "A" | "B" | "C" | "D";

// 类型收窄:从宽类型到窄类型
function process(value: string | number | boolean) {
    if (typeof value === "string") {
        // 在这里,value的类型被收窄为string
        console.log(value.toUpperCase());
    } else if (typeof value === "number") {
        // 在这里,value的类型被收窄为number
        console.log(value.toFixed(2));
    } else {
        // 在这里,value的类型是boolean
        console.log(value ? "真" : "假");
    }
}

process("hello");   // HELLO
process(3.14159);   // 3.14
process(true);      // 真

5.1.3 为什么联合类型用 |

5.1.3.1 | 在数学上是「并集」符号;TypeScript 参考了数学集合论的语言

在数学中,| 代表集合的并集。TypeScript采用了这个符号:

1
2
3
4
5
6
7
8
// 集合论中的A ∪ B
// TypeScript中对应 A | B
type A = string;
type B = number;
type Union = A | B; // string | number

// 多个并集
type Status = "pending" | "active" | "failed";

5.1.4 联合类型的方法访问规则

5.1.4.1 联合类型的值只能访问所有成员共有的方法/属性(交集)

1
2
3
4
5
6
// string | number 只能访问两者共有的成员
let value: string | number = Math.random() > 0.5 ? "hello" : 42;

console.log(value.toString()); // OK —— toString是string和number共有的
// console.log(value.toUpperCase()); // 错误!toUpperCase只有string有
// console.log(value.toFixed(2)); // 错误!toFixed只有number有

这是一个重要的规则:联合类型的值只能访问"所有成员共有的"方法。这确保了代码的安全性——无论实际值是什么类型,访问的方法一定存在。

5.1.4.2 示例:string | number 只能访问 .toString()(两者共有),不能访问 .split()(string 独有)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function process(value: string | number) {
    // 两者共有的方法
    console.log(value.toString()); // OK
    console.log(value.valueOf());  // OK
    
    // 只有string有
    // value.split(); // 错误!
    // value.toUpperCase(); // 错误!
    
    // 只有number有
    // value.toFixed(2); // 错误!
    
    // 类型收窄后就可以访问了
    if (typeof value === "string") {
        console.log(value.split("")); // OK!在string分支里
        console.log(value.toUpperCase()); // OK!
    } else {
        console.log(value.toFixed(2)); // OK!在number分支里
    }
}

5.1.5 字面量联合类型

5.1.5.1 用字面量(字符串、数字、布尔)作为联合成员:type Direction = 'Up' | 'Down' | 'Left' | 'Right'

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 字符串字面量联合类型
type Direction = "Up" | "Down" | "Left" | "Right";

function move(dir: Direction) {
    console.log("移动方向:" + dir);
}

move("Up");    // OK
move("Down");  // OK
move("Left");  // OK
move("Right"); // OK
move("Diagonal"); // 错误!"Diagonal"不在允许的范围内

5.1.5.2 应用场景:模拟枚举(当枚举过于重型时)、状态机、有限集合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 状态机
type OrderStatus = 
    | "pending"    // 待处理
    | "paid"       // 已支付
    | "shipped"    // 已发货
    | "delivered"  // 已送达
    | "cancelled"; // 已取消

function getStatusMessage(status: OrderStatus): string {
    switch (status) {
        case "pending":   return "订单待处理";
        case "paid":     return "订单已支付";
        case "shipped":  return "商品已发货";
        case "delivered": return "商品已送达";
        case "cancelled": return "订单已取消";
    }
}

console.log(getStatusMessage("pending")); // 订单待处理

5.1.5.3 类型收窄后,TS 将联合成员作为独立原子类型处理,可精确匹配 switch case

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type Shape = 
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
        case "triangle":
            return (shape.base * shape.height) / 2;
    }
}

const circle = { kind: "circle" as const, radius: 5 };
const rectangle = { kind: "rectangle" as const, width: 4, height: 6 };

console.log(getArea(circle));    // 78.54
console.log(getArea(rectangle)); // 24

5.2 交叉类型(Intersection Types)

如果说联合类型是"或",那交叉类型就是"且"——值必须同时满足所有类型


5.2.1 交叉类型的声明:A & B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 交叉类型:必须同时满足A和B
type A = { name: string };
type B = { age: number };
type AB = A & B; // { name: string; age: number }

const obj: AB = {
    name: "孙悟空",
    age: 500
};

console.log(obj.name); // 孙悟空
console.log(obj.age); // 500

5.2.2 交叉类型的语义

5.2.2.1 逻辑「且」:值必须同时属于 A 和 B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface CanWalk { walk(): void; }
interface CanSwim { swim(): void; }

// 交叉类型:同时具备两种能力
type Amphibian = CanWalk & CanSwim;

const frog: Amphibian = {
    walk() {
        console.log("跳着走");
    },
    swim() {
        console.log("游泳");
    }
};

frog.walk(); // 跳着走
frog.swim(); // 游泳

5.2.3 联合类型与交叉类型的组合(分配律)

5.2.3.1 并集对交集的分配律:(A | B) & C(A & C) | (B & C)(单向成立)

这是数学中的分配律,TypeScript也遵循:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// (string | number) & boolean
// 分配律:string & boolean | number & boolean
// 结果是 (string & boolean) | (number & boolean)
type Result = (string | number) & boolean;

// 展开后:
// = (string & boolean) | (number & boolean)
// = never | never (因为字符串和数字都不可能是boolean)
// = never

// 实际验证
let value: Result = "hello" as any; // 实际上是never

// 但如果是函数类型,行为会不同
type F1 = ((x: string) => void) | ((x: number) => void);
type F2 = ((x: string) => void) & ((x: number) => void);

// F1:可以是接受string的函数,或者接受number的函数
// F2:必须是同时接受string和number的函数(矛盾,所以是never)

5.2.3.2 示例:(string | number) & object 的实际含义

(string | number) & object 根据分配律展开为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 分配律展开
type Result = (string | number) & object;
// 第一步:应用分配律
// = (string & object) | (number & object)
// 第二步:计算每个交叉
// string & object = never —— 字符串是原始类型,不是对象,永远不可能同时是"字符串"又是"对象"
// number & object = never —— 数字也是原始类型,同样不可能
// 第三步:never | never = never
// 最终结果 = never
type Final = (string | number) & object; // never

为什么 string & object = never

在TypeScript的结构化类型系统中,string是原始类型(值类型),而object代表引用类型。两者在结构上永远不可能兼容——一个值不可能同时是"字符串"又是"对象"。

1
2
3
4
5
6
// 验证
type T1 = string & object;  // never —— string永远不是object
type T2 = number & object;  // never —— number永远不是object

// 所以 (string | number) & object = never | never = never
type Final = (string | number) & object; // never

💡 一个更实际的例子:假设有一个API返回类型是User | null & Serializable,这个交集操作要求值必须同时是User类型且实现了Serializable接口——这时交叉类型就派上用场了。

5.2.3.3 反向分配(交集对并集)不一定成立:(A & B) | CA & (B | C)

1
2
3
4
5
6
7
// 验证:分配律不是双向的
type LHS = (string & number) | boolean; // never | boolean = boolean
type RHS = string & (number | boolean);  // string & (number | boolean) = (string & number) | (string & boolean) = never | never = never

// LHS = boolean
// RHS = never
// 两者不相等!

5.2.4 为什么需要交叉类型

5.2.4.1 JavaScript 对象可以动态混合多个来源的属性;Mixins 模式需要「同时满足多个类型」的表达能力

JavaScript的对象可以随时添加任意属性,TypeScript的交叉类型正好可以描述这种灵活性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Mixins模式:用交叉类型混合多个功能
function withLog<T extends object>(obj: T): T & { log(): void } {
    return {
        ...obj,
        log() {
            console.log("log:", JSON.stringify(obj));
        }
    } as T & { log(): void };
}

function withTimestamp<T extends object>(obj: T): T & { timestamp: Date } {
    return {
        ...obj,
        timestamp: new Date()
    } as T & { timestamp: Date };
}

const base = { name: "孙悟空", age: 500 };
const enhanced = withLog(withTimestamp(base));

console.log(enhanced.name);     // 孙悟空
console.log(enhanced.timestamp); // 当前时间
enhanced.log();                  // log: {"name":"孙悟空","age":500}

5.2.5 交叉类型的实际用途

5.2.5.1 Mixins 混入模式:同时混入多个能力到同一个对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Mixin函数
function Timestamped<T extends object>(Base: T) {
    return class extends Base {
        timestamp = new Date();
    };
}

function Serializable<T extends object>(Base: T) {
    return class extends Base {
        serialize() {
            return JSON.stringify(this);
        }
    };
}

// 组合多个Mixin
class User {
    name: string;
}

const TimestampedUser = Timestamped(Serializable(User));
const user = new TimestampedUser();
user.name = "Tom";
console.log(user.timestamp); // 当前时间
console.log(user.serialize()); // {"name":"Tom","timestamp":"..."}

5.2.5.2 配置合并:多个配置源交叉后得到完整配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 默认配置
type DefaultConfig = {
    timeout: number;
    retries: number;
    debug: boolean;
};

// 开发环境配置
type DevConfig = {
    debug: true;
    logLevel: "debug" | "info";
};

// 生产环境配置
type ProdConfig = {
    debug: false;
    logLevel: "warn" | "error";
};

// 开发配置 = 默认配置 + 开发特定配置
type DevFullConfig = DefaultConfig & DevConfig;
const devConfig: DevFullConfig = {
    timeout: 5000,
    retries: 3,
    debug: true,
    logLevel: "debug"
};

// 生产配置 = 默认配置 + 生产特定配置
type ProdFullConfig = DefaultConfig & ProdConfig;
const prodConfig: ProdFullConfig = {
    timeout: 10000,
    retries: 1,
    debug: false,
    logLevel: "error"
};

5.2.5.3 类型扩展:为已有类型动态添加额外属性(如 User & { role: 'admin' }

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

// 扩展User类型
type Admin = User & {
    role: "admin";
    permissions: string[];
};

const admin: Admin = {
    name: "管理员",
    email: "admin@example.com",
    role: "admin",
    permissions: ["read", "write", "delete"]
};

console.log(admin.role); // admin

5.3 可辨识联合(Tagged Union / Discriminated Union)

可辨识联合是TypeScript中处理多态类型的一种强大模式。它结合了联合类型和类型收窄,让你能够安全地处理多种可能的状态。


5.3.1 可辨识联合的概念与实现

5.3.1.1 用一个公共字面量属性(判别属性)区分联合成员

可辨识联合的关键是判别属性(Discriminant Property)——一个所有联合成员都有的、类型为字面量类型的公共属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 定义三种不同的事件类型
interface SuccessEvent {
    type: "success";      // 判别属性:字面量类型
    data: unknown;
    timestamp: Date;
}

interface ErrorEvent {
    type: "error";         // 判别属性
    error: Error;
    timestamp: Date;
}

interface LoadingEvent {
    type: "loading";       // 判别属性
    message: string;
}

// 联合类型
type AppEvent = SuccessEvent | ErrorEvent | LoadingEvent;

// 使用:TypeScript会根据type自动收窄
function handleEvent(event: AppEvent) {
    switch (event.type) {
        case "success":
            console.log("成功!数据:", event.data);    // event是SuccessEvent
            console.log("时间:", event.timestamp);
            break;
        case "error":
            console.error("错误:", event.error.message); // event是ErrorEvent
            console.log("时间:", event.timestamp);
            break;
        case "loading":
            console.log("加载中:", event.message);     // event是LoadingEvent
            break;
    }
}

// 测试
handleEvent({
    type: "success",
    data: { id: 1 },
    timestamp: new Date()
});
// 输出:成功!数据:{ id: 1 } 时间:2026-03-26T...

handleEvent({
    type: "error",
    error: new Error("网络错误"),
    timestamp: new Date()
});
// 输出:错误:网络错误 时间:2026-03-26T...

5.3.1.2 type Action = { type: 'increment'; delta: number } | { type: 'decrement'; delta: number }

这个模式在Redux等状态管理库中非常常见:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Redux风格的Action
type CounterAction =
    | { type: "increment"; delta: number }
    | { type: "decrement"; delta: number }
    | { type: "reset" };

function reducer(state: number, action: CounterAction): number {
    switch (action.type) {
        case "increment":
            return state + action.delta; // action.delta存在
        case "decrement":
            return state - action.delta; // action.delta存在
        case "reset":
            return 0;
    }
}

console.log(reducer(10, { type: "increment", delta: 5 })); // 15
console.log(reducer(10, { type: "decrement", delta: 3 })); // 7
console.log(reducer(10, { type: "reset" }));               // 0

5.3.2 可辨识联合的穷举检查

5.3.2.1 default 分支返回 never;若漏掉 case,编译时报错

这是可辨识联合最强大的特性——穷举检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
        case "triangle":
            return (shape.base * shape.height) / 2;
        default:
            // 穷举检查:如果漏掉了某个case,TS会报错
            const _exhaustive: never = shape;
            throw new Error("不可能的形状:" + JSON.stringify(_exhaustive));
    }
}

如果以后添加了新的Shape变体但忘记处理,TypeScript会在default分支报错:

1
2
3
4
5
6
7
8
9
// 添加新形状
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; base: number; height: number }
    | { kind: "ellipse"; a: number; b: number }; // 新增

// TS会在getArea的default分支报错:
// Type '{ kind: "ellipse"; a: number; b: number; }' is not assignable to type 'never'.

5.3.3 为什么叫「可辨识」

5.3.3.1 判别属性让 TypeScript 能区分联合成员;类似代数数据类型(ADT)的概念

“可辨识"这个名字来自于判别属性能够"辨识"每个联合成员:

graph TD
    A[AppEvent 联合类型] --> B[SuccessEvent<br/>type: "success"]
    A --> C[ErrorEvent<br/>type: "error"]
    A --> D[LoadingEvent<br/>type: "loading"]
    
    B --> E[通过 type === 'success'<br/>识别为SuccessEvent]
    C --> F[通过 type === 'error'<br/>识别为ErrorEvent]
    D --> G[通过 type === 'loading'<br/>识别为LoadingEvent]

这个概念来自代数数据类型(Algebraic Data Types,ADT),在Haskell、F#、Rust等语言中很常见。TypeScript的可辨识联合是ADT思想在JavaScript生态中的实现。


5.3.4 应用场景:Redux/状态管理、网络请求结果(SuccessResponse | ErrorResponse)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 网络请求结果
type ApiResult<T> =
    | { status: "success"; data: T; code: 200 }
    | { status: "client_error"; error: string; code: 400 | 401 | 403 }
    | { status: "server_error"; error: string; code: 500 }
    | { status: "network_error"; error: string };

async function fetchUser(id: number): Promise<ApiResult<{ name: string; age: number }>> {
    try {
        const response = await fetch(`/api/user/${id}`);
        if (!response.ok) {
            return { status: "client_error", error: "请求失败", code: response.status as 400 | 401 | 403 };
        }
        const data = await response.json();
        return { status: "success", data, code: 200 };
    } catch (err) {
        return { status: "network_error", error: "网络连接失败" };
    }
}

// 使用
async function main() {
    const result = await fetchUser(1);
    
    switch (result.status) {
        case "success":
            console.log("用户信息:", result.data);
            break;
        case "client_error":
            console.error("客户端错误:", result.error, "状态码:", result.code);
            break;
        case "server_error":
            console.error("服务器错误:", result.error, "状态码:", result.code);
            break;
        case "network_error":
            console.error("网络错误:", result.error);
            break;
    }
}

main();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 状态机
type TrafficLight =
    | { state: "red"; duration: number }
    | { state: "yellow"; duration: number }
    | { state: "green"; duration: number };

function getNextLight(current: TrafficLight): TrafficLight {
    switch (current.state) {
        case "red":
            return { state: "green", duration: 3000 };
        case "green":
            return { state: "yellow", duration: 1000 };
        case "yellow":
            return { state: "red", duration: 5000 };
    }
}

let light: TrafficLight = { state: "red", duration: 5000 };
for (let i = 0; i < 3; i++) {
    console.log(`当前灯:${light.state},持续${light.duration}ms`);
    light = getNextLight(light);
}

📝 本节小结:可辨识联合(Tagged Union)通过一个公共的字面量属性(判别属性)来区分联合成员。TypeScript会根据判别属性的值自动收窄类型,从而安全地访问每个成员特有的属性。可辨识联合配合switch语句和穷举检查,可以确保所有可能的情况都被处理。添加新的联合成员时,TypeScript会在编译时报错,提示需要处理新的情况。


本章小结

本章学习了TypeScript中三种强大的类型组合工具:联合类型、交叉类型和可辨识联合。

联合类型|表示"或"的关系,值可以是任意一个成员类型,但同时只能是一个。联合类型的值只能访问所有成员共有的方法/属性。字面量联合类型适合表示有限集合、状态机等场景。类型收窄可以逐步缩小联合类型的范围。

交叉类型&表示"且"的关系,值必须同时满足所有类型。交叉类型常用于Mixins模式(混入多个功能)、配置合并(多个配置源交叉)、类型扩展。分配律在联合类型和交叉类型之间单向成立。

可辨识联合(Tagged Union)是处理多态类型的强大模式。它通过一个公共的字面量属性(判别属性)来区分联合成员。TypeScript会根据判别属性的值自动收窄类型。可辨识联合配合穷举检查和default返回never,可以确保所有情况都被处理,添加新成员时编译器会报错。

这三种类型组合工具是TypeScript类型系统的核心,掌握它们可以写出更精确、更安全的类型代码。


恭喜你完成了TypeScript核心类型的全部内容!从原始类型到特殊类型,从接口到类型别名,从联合类型到可辨识联合——你已经具备了TypeScript类型系统的坚实基础。

下一阶段的内容将是更高级的TypeScript特性,包括泛型、类型操作符、条件类型、映射类型等。继续保持这个学习节奏,你正在成为一个TypeScript高手!