TypeScript 如何校验数组穷尽第三方字符串字面量联合类型的所有取值
方案:类型系统实现字面量联合类型的数组穷尽校验
你之前写的const iconNames: IconName[] = [...]本质是将数组标记为IconName的子集,TS只会校验单个元素是否合法,完全不会检查是否穷尽了所有IconName的成员——哪怕数组只写了一个元素,类型检查也会通过。
我们可以完全不修改第三方定义的IconName,也不用反转类型推导流程,通过泛型捕获、严格类型相等校验的能力,实现「元素全合法」「无遗漏无多余」的双重校验。
第一步:准备必要的类型工具
首先定义两个核心的类型工具,用来完成校验逻辑:
- 严格类型相等判断:规避TS结构类型的兼容误判,确保两个类型完全一致才返回匹配
type IsExactMatch<T, U> = (<G>() => G extends T ? 1 : 2) extends (<G>() => G extends U ? 1 : 2) ? true : false;
- 数组元素联合类型提取:拿到当前数组所有元素去重后构成的联合类型
type ArrayElementUnion<T extends readonly unknown[]> = T[number];
如果需要带友好提示的报错,还可以补充两个工具,用来定位具体的缺失、多余元素:
// 计算当前数组遗漏的IconName成员 type MissingElements<T extends readonly IconName[]> = Exclude<IconName, ArrayElementUnion<T>>; // 计算当前数组多出的非IconName元素(兜底用,一般会被前置约束拦截) type ExtraElements<T extends readonly unknown[]> = Exclude<ArrayElementUnion<T>, IconName>;
第二步:封装带校验的数组创建函数
写一个泛型函数,把校验逻辑内置进去,后续定义图标列表的时候直接调用即可:
function createIconList<T extends readonly IconName[]>( list: T & ( // 只有数组元素和IconName完全匹配时,才通过校验 IsExactMatch<ArrayElementUnion<T>, IconName> extends true ? unknown // 不匹配时抛出带具体缺失元素的明确报错 : `校验不通过:请核对图标列表,缺失或多余的项为:${MissingElements<T> | ExtraElements<T>}` ) ): T { return list; }
这个函数的双重校验逻辑:
- 泛型的前置约束
T extends readonly IconName[],首先保证数组里的每一个元素都是合法的IconName值,如果写了一个不在IconName范围内的字符串,会在书写阶段直接报错。 - 内置的
IsExactMatch校验,保证数组的元素构成的联合类型,和第三方定义的IconName完全相等——既不能漏掉任何一个IconName成员,也不能加入额外的非合法值。
实际使用方式
直接调用函数传入数组即可,TS会自动完成校验:
const iconNames = createIconList([ "house", "user", "gear", // 补全所有IconName的成员 ]);
如果遗漏了某个图标,比如漏掉了"xmark",TS会直接在数组定义的位置抛出清晰的提示:
类型“readonly ["house", "user", "gear"]”的参数不能赋给类型“unknown & "校验不通过:请核对图标列表,缺失或多余的项为:xmark"”的参数。
你可以顺着报错把所有缺失项补完,直到没有报错为止。
可选优化:校验重复元素
因为联合类型会自动去重,上面的逻辑不会拦截数组里重复书写的同一个图标(比如写了两次"house")。如果需要严格去重,可以再加一层长度校验:
首先补充计算字符串联合类型成员数的工具:
// 计算字符串联合类型的不重复成员总数量 type UnionMemberCount<T extends string, Acc extends string[] = []> = T extends infer First ? Exclude<T, First> extends never ? [...Acc, First]["length"] : UnionMemberCount<Exclude<T, First>, [...Acc, First]> : never;
然后修改createIconList的校验逻辑,增加长度判断:
function createIconList<T extends readonly IconName[]>( list: T & ( IsExactMatch<ArrayElementUnion<T>, IconName> extends true ? // 校验数组长度和IconName的总成员数相等,拦截重复元素 T["length"] extends UnionMemberCount<IconName> ? unknown : `校验不通过:数组存在重复元素,请去重后重新核对` : `校验不通过:请核对图标列表,缺失或多余的项为:${MissingElements<T> | ExtraElements<T>}` ) ): T { return list; }
内容的提问来源于stack exchange,提问作者chadlavi
相关产品推荐
相关产品推荐

