You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 01:23:20