本文介绍如何利用 TypeScript 的高级类型技巧(如递归条件类型和元组联合转换),自动校验数组是否完整包含 as const 对象的所有键,实现“增删枚举项即触发编译错误”的强约束机制。
本文介绍如何利用
typescript
的高级类型技巧(如递归条件类型和元组联合转换),自动校验数组是否完整包含 `as const` 对象的所有键,实现“增删枚举项即触发编译错误”的强约束机制。
在 TypeScript 项目中,我们常使用 as const 对象模拟字符串枚举(如状态码、选项标识等),以兼顾运行时可读性与编译期类型安全性。但当需要将这些键导出为数组(例如用于下拉选项、权限校验或 API 参数白名单)时,传统写法极易遗漏新增/删除的键 —— 而 TypeScript 默认无法检测 ['ACTIVE'] 是否缺少 'DELETED'。
下面是一个典型且
不安全
的写法:
该代码不会报错,因为 StatusKeys[] 仅表示“元素属于联合类型”,而非“必须包含全部字面量”。
✅ 正确解法:用递归条件类型生成全排列元组
核心思路是:
将联合类型 'ACTIVE' | 'DELETED' 转换为唯一合法元组类型 ['ACTIVE', 'DELETED'] 或 ['DELETED', 'ACTIVE']
,从而让 TypeScript 在赋值时强制要求“数量一致 + 元素全覆盖”。
我们定义一个泛型工具类型 TupleUnion:
TypeScript-45分钟入门课件源码
TypeScript-45分钟入门课件源码
下载
该类型通过映射类型遍历联合中的每个字面量 S,并递归构建所有可能的排列组合元组,最终通过索引访问 [U] 得到
所有合法排列的联合类型
。
应用示例:
?
原理简析
:TupleUnion<'A' | 'B'> 展开后等价于 ['A', 'B'] | ['B', 'A'],因此赋值时若元素数量不足或存在重复/缺失,TS 会因无法匹配任一元组而报错。
⚠️ 注意事项与最佳实践
性能提示
:TupleUnion 是递归类型,对超过 5–6 个字面量的联合可能触发 TS 深度限制(可通过 --maxNodeModuleJsDepth 调整,但建议枚举项保持精简)。
顺序无关性
:该方案允许任意顺序,符合多数业务场景;如需固定顺序(如按定义顺序),需配合 const 断言 + satisfies(TS 4.9+)或额外排序逻辑。
维护性优势
:一旦向 Status 新增 'ARCHIVED': 'Archived',所有依赖 TupleUnion 的数组声明将
立即报错
,强制开发者同步更新,杜绝漏配风险。
替代方案对比
:
Object.keys(Status) as const ❌ 无法推导字面量类型;
手动维护 ['ACTIVE', 'DELETED'] as const ❌ 失去与源对象的自动同步;
运行时校验(如 assert(ArrayStatus.length === Object.keys(Status).length))❌ 无法在编辑器/CI 阶段捕获。
通过 TupleUnion,你将获得真正“声明即契约”的类型保障 —— 枚举即真相,数组即镜像,任何偏差皆不可编译通过。
export const Status = {
'ACTIVE': 'Active',
'DELETED': 'Deleted'
} as const;
export type StatusKeys = keyof typeof Status; // 'ACTIVE' | 'DELETED'
// ❌ 编译通过,但语义不完整:缺少 'DELETED'
export const ArrayStatus: StatusKeys[] = ['ACTIVE'];type TupleUnion = {
[S in U]: Exclude extends never
? [...R, S] // 基础情况:只剩一个未选,直接追加
: TupleUnion, [...R, S]>; // 递归:从剩余中选一个,继续构造
}[U]; export const Status = {
'ACTIVE': 'Active',
'DELETED': 'Deleted',
'PENDING': 'Pending'
} as const;
export type StatusKeys = keyof typeof Status; // 'ACTIVE' | 'DELETED' | 'PENDING'
// ✅ 正确:必须包含全部三个键(顺序任意)
export const ArrayStatus: TupleUnion = ['ACTIVE', 'PENDING', 'DELETED'];
// ❌ 编译错误:缺少 'PENDING'
// export const ArrayStatusBad: TupleUnion = ['ACTIVE', 'DELETED'];
// ✅ 也可用于子集(需显式排除)
export const Subset: TupleUnion> = ['ACTIVE', 'DELETED']; 