TypeScript如何限制元组中特定联合类型成员仅出现一次?
问题:限制元组中特定类型最多出现一次
我想编写一个接受联合类型元组的函数,要求联合类型中的特定成员仅能出现一次(或不出现)。比如[A, B, A]合法,但[A, B, A, B]不合法。理想写法是[...A[], B, ...A[]] | A[],但TypeScript不支持这种语法。
我尝试了一种递归验证的方案,通过泛型交叉unknown或带错误信息的对象来约束,但单独使用EnsureOne类型时符合预期,和元组交叉作为函数参数约束时却没效果。示例代码如下:
type A = number; type B = string; type EnsureOne<Tuple, T, Seen = false> = Tuple extends [infer Head, ...infer Tail] ? Head extends T ? Seen extends true ? { ERROR: [`Only one value of type`, T, `is allowed`] } : EnsureOne<Tail, T, true> : EnsureOne<Tail, T, Seen> : unknown; type ExpectUnknown = EnsureOne<[A, B, A], B>; // ^? unknown type ExpectError = EnsureOne<[A, B, A, B], B>; // ^? { ERROR: ["Only one value of type", string, "is allowed"] } type Union = A | B; declare function f<const T extends readonly Union[]>(t: T & EnsureOne<T, B>): void; const expectError = f([0, '', '']); // 这里没有触发预期报错 :(
单独使用EnsureOne能正确识别合法/非法元组,但作为函数参数的交叉约束时,TypeScript没有触发预期的错误。
解决办法
问题出在TypeScript对泛型约束的处理逻辑上:当T被约束为readonly Union[]时,交叉EnsureOne<T, B>不会被严格校验。我们需要调整EnsureOne的返回值逻辑,并修改函数的泛型约束方式,让TypeScript在推断T时就强制校验合法性:
方案一:修改EnsureOne返回值并约束泛型
type A = number; type B = string; type EnsureOne<Tuple, T, Seen = false> = Tuple extends [infer Head, ...infer Tail] ? Head extends T ? Seen extends true ? never : // 非法时返回never,直接阻断类型匹配 EnsureOne<Tail, T, true> : EnsureOne<Tail, T, Seen> : Tuple; // 合法时返回原元组类型,保留完整类型信息 type Union = A | B; declare function f<const T extends readonly Union[] & EnsureOne<T, B>>(t: T): void; const valid1 = f([0, '', 1]); // 合法,无报错 const valid2 = f([0, 1, 2]); // 合法(不含B类型) const invalid = f([0, '', '']); // 触发报错:类型“readonly [number, string, string]”不满足约束“EnsureOne<readonly [number, string, string], string>”
方案二:通过条件类型直接约束参数类型
type A = number; type B = string; type EnsureOne<Tuple, T, Seen = false> = Tuple extends [infer Head, ...infer Tail] ? Head extends T ? Seen extends true ? never : EnsureOne<Tail, T, true> : EnsureOne<Tail, T, Seen> : unknown; type Union = A | B; declare function f<const T extends readonly Union[]>( t: EnsureOne<T, B> extends never ? never : T ): void; const valid1 = f([0, '', 1]); // 合法 const valid2 = f([0, 1, 2]); // 合法 const invalid = f([0, '', '']); // 触发报错:类型“[number, string, string]”的参数不能赋给类型“never”的参数
两种方案的核心逻辑都是让非法元组对应的类型变为never,从而让TypeScript在函数调用时直接识别错误。
内容的提问来源于stack exchange,提问作者Tao
相关产品推荐
相关产品推荐

