【Typescript】05-联合类型交叉类型与类型别名
联合类型、交叉类型与类型别名
到这一篇,TypeScript 才真正开始像一门“类型语言”。前面的基础类型、对象、函数、接口,更多是在建立类型意识;而联合类型与交叉类型,则开始让你具备表达复杂业务关系的能力。
很多人学习 TypeScript 时,真正的分水岭就出现在这里。因为从这一刻起,你不再只是在回答“一个值是什么类型”,而是在回答更复杂的问题:
- 一个值可能处于哪些合法形态
- 一个结构需要同时具备哪些特征
- 一个类型表达式是否值得被命名和复用
这也是 TypeScript 从“带标注的 JavaScript”进入“有建模能力的类型系统”的关键一步。
联合类型:表达“几种可能之一”
最基础的联合类型写法如下:
let value: string | number;
value = "hello";
value = 123;
这表示 value 的合法范围不是单一类型,而是多种类型中的一种。联合类型非常适合表达“不确定但有限”的输入和状态。
最常见的例子是状态建模:
type LoadingState = "idle" | "loading" | "success" | "error";
这行代码看起来很简单,但它已经比 string 强很多,因为它把状态空间从“任意字符串”缩小成了“真实业务允许的四种值”。
联合类型在实际项目里为什么这么重要
因为现实业务中,大量值并不是单一形态,而是“多种合法结果之一”。例如:
- 一个函数参数可能接收
string或number - 一个接口响应可能成功,也可能失败
- 一个 UI 状态可能是
loading、empty、success、error - 一个组件 prop 可能允许不同配置模式
如果不用联合类型,你只能:
- 要么写成特别宽泛的类型
- 要么靠注释和约定表达这些分支
这两种方式都不如显式建模稳定。
联合类型的限制,恰恰是它的价值
看下面这个例子:
function printId(id: string | number) {
console.log(id.toUpperCase());
}
这段代码会报错。原因不是 TypeScript 太刻板,而是它在坚持一个很合理的原则:你只能直接使用所有候选类型都共同拥有的能力。
因为在 string | number 里,toUpperCase() 只属于 string,不属于 number。所以在你没有进一步判断之前,编译器拒绝你这么做。
这也是为什么联合类型经常会和“类型缩小”绑定出现。联合类型负责表达可能性,类型缩小负责在代码分支里确认当前到底是哪一种。
交叉类型:表达“同时满足多个约束”
交叉类型用 & 表示:
type BaseUser = {
id: number;
name: string;
};
type WithTimestamps = {
createdAt: string;
updatedAt: string;
};
type User = BaseUser & WithTimestamps;
这里的 User 表示它既要满足 BaseUser,也要满足 WithTimestamps。和联合类型不同,交叉类型不是几选一,而是约束叠加。
这在真实项目里很常见,例如:
- 基础实体 + 审计字段
- 领域对象 + 权限信息
- 业务数据 + 分页元信息
- 组件 props + 通用样式属性
交叉类型很适合做“特征组合”
你可以把它理解成积木拼接,但这个比喻有一个前提:拼接的几块积木之间不能彼此冲突。
例如:
type WithId = {
id: number;
};
type WithName = {
name: string;
};
type User = WithId & WithName;
这里的组合就很自然,因为两个结构互补。
但交叉类型不是万能拼接器
很多初学者第一次接触 & 时,会以为它就是“把两个对象简单合并”。大多数互补场景下确实接近这样,但如果字段冲突,结果往往没有你想得那么直观。
type A = { value: string };
type B = { value: number };
type C = A & B;
这里的 C["value"] 不会 magically 变成“字符串或数字”,而会进入一种几乎不可满足的状态,因为一个值不可能同时既是 string 又是 number。
所以一个非常实用的工程判断是:交叉类型适合合并互补约束,不适合硬拼彼此冲突的模型。
type 的价值,不只是给对象命名
很多人最开始把 type 只当成“另一种写对象的方式”,其实它更大的价值在于:可以给任意类型表达式起别名。
type ID = string | number;
type Point = [number, number];
type Handler = (message: string) => void;
type RequestStatus = "idle" | "loading" | "success" | "error";
这件事很重要,因为一旦类型表达式开始变复杂,命名本身就是在提升可读性。
对比下面两种写法:
function findUser(id: string | number) {}
type UserId = string | number;
function findUser(id: UserId) {}
第二种更易读,也更利于复用。因为“联合类型本身”只是语法,“命名后的类型别名”才更接近系统里的业务概念。
一个典型场景:用联合类型表达接口结果
假设一个登录接口要么成功,要么失败:
type LoginSuccess = {
success: true;
token: string;
userId: number;
};
type LoginFailure = {
success: false;
message: string;
};
type LoginResult = LoginSuccess | LoginFailure;
这就是联合类型最有力量的地方。你不必用一堆可选字段去糊一个宽对象,而是明确告诉系统:返回值只有两种合法形态。
相比下面这种写法:
type LoginResult = {
success: boolean;
token?: string;
userId?: number;
message?: string;
};
前者的表达能力强得多,因为它保留了状态和字段之间的真实关系。
一个典型场景:用交叉类型叠加通用字段
type Entity = {
id: number;
};
type Timestamps = {
createdAt: string;
updatedAt: string;
};
type Article = Entity & Timestamps & {
title: string;
content: string;
};
这类组合在工程里非常常见。它不是为了炫技,而是为了减少重复,并让公共结构保持统一。
如何判断该用联合还是交叉
这是一个非常值得反复练习的判断:
- 如果你表达的是“几种合法形态之一”,用联合类型
- 如果你表达的是“同时具备多个特征”,用交叉类型
- 如果一个类型表达式会反复出现,给它起一个别名
你也可以把它翻译成更口语的版本:
- “或者”通常对应联合
- “并且”通常对应交叉
初学者常见误区
误区一:联合类型写出来后,直接按某一类使用
这会立刻遇到报错。不是 TypeScript 在找麻烦,而是你还没有告诉编译器当前到底是哪一种情况。
误区二:交叉类型被当成“对象合并万金油”
如果两个模型本身矛盾,交叉后的结果只会更难用,不会更强大。
误区三:复杂类型不命名
一长串联合、交叉、函数签名如果没有名字,代码会很快失去可读性。类型别名很多时候不是可选项,而是沟通工具。
本文小结
联合类型和交叉类型,是 TypeScript 表达复杂业务关系的基础工具。联合类型让你描述“这个值可能是哪几种合法形态之一”,交叉类型让你描述“这个值必须同时满足哪些约束”。而 type 作为类型别名,则让这些表达可以被命名、复用和沟通。
从这一篇开始,你应该逐渐放弃“变量是什么类型”的单点思维,转向“一个业务对象可能处于哪些合法状态”的系统思维。这才是 TypeScript 真正开始有建模力量的地方。
练习
- 定义一个
Result类型,表示"ok"或"error",再扩展成一个真正可用的成功/失败对象联合。 - 定义
UserBase和UserProfile,然后用交叉类型合成UserDetail。 - 思考登录接口返回成功和失败两种结构时,为什么联合类型通常比“一个大对象里全是可选字段”更合适。
后记
2026年5月21日于上海。
更多推荐
所有评论(0)