数组、元组、枚举与字面量类型
这一篇要讨论四个看似接近、实际上经常被混用的概念:数组、元组、枚举和字面量类型。它们都和"值的集合"有关,但解决的是完全不同的问题。很多项目里的类型建模之所以写得模糊,往往不是不会写,而是没有分清这些工具各自表达的到底是什么。
从工程角度看,这一篇非常重要。因为你接下来会越来越频繁地遇到这些场景:
- 一组同类数据要怎么表示
- 一组固定顺序的数据要怎么表示
- 一组有限状态要怎么限制
- 一组固定选项是用
enum还是别的方式
如果这些基础分不清,后面的联合类型和判别联合也会变得别扭。
数组表达的是"同类的一批数据"
最常见的数组写法有两种:
ts
const scores: number[] = [90, 88, 95];
const tags: Array<string> = ["ts", "js", "node"];
这两种语法在 TypeScript 里是等价的。重点不在形式,而在含义:数组表示元素类型一致,但数量不固定。
也就是说,当你写 number[] 时,实际在说:
- 这个集合里可以有很多值
- 每个值都应该是
number - 具体有几个元素并不重要
这非常适合:
- 成绩列表
- 标签列表
- 用户列表
- 接口返回的分页数据项
混合类型数组通常意味着结构不够清晰
ts
const values = [1, "hello", true];
这时 TypeScript 会推断成 (number | string | boolean)[]。从语法上说这是合法的,但从建模角度看,通常说明这个结构还没想清楚。
因为一个数组如果什么都能塞,后续使用时就会非常难受。你要不断判断当前元素到底是什么类型,代码既啰嗦又脆弱。
更常见也更合理的做法往往是:
- 要么拆成对象
- 要么拆成多个更清晰的数组
- 要么确认这其实是元组,不是数组
元组表达的是"每个位置都有意义"
ts
const point: [number, number] = [100, 200];
const userInfo: [number, string, boolean] = [1, "Alice", true];
元组和数组最本质的区别,不是写法,而是建模意图:
- 数组强调"同类批量"
- 元组强调"固定位置、固定长度、每个位置含义不同"
例如一个二维坐标很适合元组,因为第一个位置一定是 x,第二个位置一定是 y。一个返回 [data, error] 的工具函数,也可能适合元组,因为这两个位置的语义非常明确。
什么时候不该用元组
元组虽然紧凑,但不是默认优选。只要一个结构里的字段开始具备明确业务语义,通常对象会比元组更可读。
比如这两个写法:
ts
const userInfo: [number, string, boolean] = [1, "Alice", true];
ts
const user = {
id: 1,
name: "Alice",
active: true
};
第二种通常更容易维护,因为你不需要记住"第三个位置是不是表示启用状态"。所以一个很实用的原则是:
- 强调顺序和位置时,用元组
- 强调字段语义时,用对象
字面量类型表达的是"有限且精确的值"
ts
let direction: "left" | "right";
direction = "left";
这里 direction 不再是任意字符串,而只能是 "left" 或 "right"。这正是字面量类型的价值:它让类型从宽泛的范围,收紧成明确的合法状态集合。
同样的思路可以用在大量业务状态中:
ts
type OrderStatus = "pending" | "paid" | "cancelled";
type ThemeMode = "light" | "dark";
type RequestState = "idle" | "loading" | "success" | "error";
这种写法比直接写 string 强得多,因为它会让非法状态在编码阶段就被阻止。
为什么字面量类型在工程里这么重要
因为真实业务里,大量字段并不是"任意字符串",而是"只能从有限集合中取值"。例如:
- 订单状态
- 用户角色
- 组件尺寸
- 请求状态
- 语言代码
如果你把这些都写成 string,等于把本该清晰的状态空间又放宽了。后续所有逻辑判断都建立在一个模糊前提上,代码会越来越难收敛。
enum 是传统方案,但不一定是现代默认方案
TypeScript 提供了 enum:
ts
enum Role {
Admin,
User,
Guest
}
或者字符串枚举:
ts
enum Status {
Pending = "pending",
Done = "done"
}
enum 的优点是语义明确,历史也很长,所以你在很多老项目和教程里都会看到它。
但在现代 TypeScript 项目里,越来越多团队更喜欢使用"对象常量 + 字面量联合"的方式:
ts
const ROLE = {
ADMIN: "admin",
USER: "user",
GUEST: "guest"
} as const;
type Role = typeof ROLE[keyof typeof ROLE];
这种写法第一次看可能会觉得啰嗦,但它有几个工程优势:
- 运行时就是普通 JavaScript 对象
- 类型和值天然保持一致
- 更方便和现代工具链协作
- 避免某些
enum编译产物上的心智负担
那我到底该用 enum 还是字面量联合
比较务实的建议是:
- 你要读懂
enum,因为历史代码里很多 - 你在新项目里,可以优先考虑字面量联合或
as const对象
这不是说 enum 不能用,而是说你要知道它不是唯一方案。很多团队选择不用 enum,并不是因为它错误,而是因为更偏向贴近 JavaScript 本体的写法。
一个很典型的建模对比
假设你要表示订单状态。下面三种写法,清晰度完全不同。
最弱的写法
ts
let status: string;
这几乎没有约束,任何字符串都能进来。
更好的写法
ts
type OrderStatus = "pending" | "paid" | "cancelled";
let status: OrderStatus;
这时状态空间已经被限制在真实业务允许的范围内。
更接近工程实践的写法
ts
const ORDER_STATUS = {
PENDING: "pending",
PAID: "paid",
CANCELLED: "cancelled"
} as const;
type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];
这样你同时拥有:
- 运行时可复用常量
- 编译期精确类型
这就是为什么现代 TypeScript 项目里,这类写法越来越普遍。
常见误区
误区一:所有多值结构都用数组
很多人看到"一组值"就先写数组,但真实需求可能是固定结构、固定顺序、有限状态,并不一定适合数组。
误区二:能写 string 就不想写字面量联合
这会让本该被限制的状态重新变宽。你短期省了几个字符,长期会多出更多判断和 bug。
误区三:把元组当成节省对象定义的捷径
元组不是为了省字段名,而是为了表达位置语义。如果一个结构需要靠注释才能知道每个位置代表什么,那它大概率更适合对象。
本文小结
数组强调的是"同类的一批数据",元组强调的是"固定位置的结构化数据",字面量类型强调的是"有限状态集合",而 enum 是 TypeScript 提供的一种传统组织方式。把这些工具分清楚,你的建模能力会立刻提升一个层级。
真正好的类型设计,往往不是写得多,而是选得准。你能准确判断"这里需要批量、这里需要位置、这里需要状态限制",后面的代码自然会更稳。
练习
- 用元组表示一个 RGB 颜色值,并思考它和对象写法各自的优缺点。
- 用字面量联合定义订单状态:
pending、paid、cancelled、refunded。 - 把一个
enum改写成对象常量加字面量联合的形式,并比较两者的使用体验。
后记
2026年5月21日于上海。