找不到某种可变参数函数的正确类型

Cannot find the proper typing for a certain kind of varargs function

请看下面的小演示:

function f<T extends string, U extends `${T}${T}`>(arg: [T, U]) {}
function g(...args: ???[]) {} // what's the right type here (-> ???)?

f(['a', 'aa']) // ok
f(['a', 'ab']) // error as expected

g(
  ['a', 'aa'], // should be okay
  ['b', 'bb'], // should be okay
  ['c', 'ab']  // should result in a compile error!!!
)

» TypeScript Playground

[编辑] 当然,我可以更改函数 g 以将 n 个可选参数与 2*n 个类型参数结合使用,但这不是我正在寻找的解决方案。

[编辑] 顺便说一下:如果函数 g 接受这些元组的一个数组而不是可变参数并相应地表现,那也是一个有用的解决方案,但我想这是同样的问题。

[编辑] 使 g 成为以下形式的单参数函数可以工作,但在语法上不是很好:

// expected error at the 'ab'
g(['a', 'aa'])(['b', 'bb'])(['c', 'ab'])

DEMO

我的方法是这样的:

function g<T extends string[]>(...args: { [I in keyof T]:
  [T[I], NoInfer<Dupe<T[I]>>]
}) { }

type Dupe<T> = T extends string ? `${T}${T}` : never;

type NoInfer<T> = [T][T extends any ? 0 : never];

想法是 g()generic in the type parameter T[], a tuple type consisting of just the string literal types 来自 args 的每对字符串的第一个元素。所以如果你调用g(["a", "aa"], ["b", "bb"], ["c", "cc"]),那么T应该是元组类型["a", "b", "c"].

args 的类型可以用 mapped tuple type 表示为 T。对于 T 元组中的每个数字索引 Iargs 的相应元素应该具有类似 [T[I], Dupe<T[I]>] 的类型,其中 Dupe<T> 将字符串连接到它们自己想要的方式。所以如果T["z", "y"],那么args的类型应该是[["z", "zz"], ["y", "yy"]].

这里的目标是编译器应该看到像 g(["x", "xx"], ["y", "oops"])infer T from the type of args 这样的调用是 ["x", "y"],然后检查 args 的类型并注意虽然 ["x", "xx"] 很好,但 ["y", "oops"] 与预期的 ["y", "yy"] 不匹配,因此出现错误。

这是基本方法,但我们需要解决一些问题。


首先,microsoft/TypeScript#27995 处有一个未解决的问题导致编译器无法识别 T[I] 肯定是映射元组类型 [=42= 中的 string ].即使映射的元组类型创建了另一个元组类型,编译器仍认为 I 可能是 any[] 的其他键,例如 "map""length",因此 T[I]可以是函数类型或 number,等等

所以如果我们定义 Dupe<T> 就像

type Dupe<T extends string> = `${T}${T}`;

会出现 Dupe<T[I]> 会导致错误的问题。为了避免这个问题,我定义了 Dupe<T>,这样它就不需要 Tstring。相反,它只是使用 conditional type 来表示“if Tstringthen Dupe<T> 将是 `${T}${T}`":

type Dupe<T> = T extends string ? `${T}${T}` : never;

这样就解决了这个问题。


下一个问题是要从 {[I in keyof T]: [T[I], Dupe<T[I]>]} 类型的值推断 T,编译器可以自由地查看每个 args 对的第一个元素(T[I]),以及第二个元素 (Dupe<T[I]>)。在该类型中每次提及 T[I] 都是 推理站点 T[I]。我们希望编译器在推断 T 时只查看每对的第一个元素,而仅 检查 第二个元素。不幸的是,如果你这样保留它,编译器会尝试同时使用两者并感到困惑。

也就是说,当从[["y", "zz"]]推断出T时,编译器会再次匹配"y"T[I]以及"zz"Dupe<T[I]>,它最终为 T 推断出 union type ["y" | "z"]。实际上,这非常聪明,因为如果 T 真的是 ["y" | "z"],那么 args 的类型将是 ["y" | "z", "yy" | "zz"] 类型,并且值 ["y", "zz"] 匹配它.好的!除了你想拒绝 [["y", "zz"]].

所以我们需要一些方法来告诉编译器不要使用每对的第二个元素来推断Tmicrosoft/TypeScript#14829 上有一个未解决的问题,需要一些支持 NoInfer<T> 形式的“非推理类型参数用法”。像 NoInfer<T> 这样的类型最终会求值为 T,但会阻止编译器尝试从中推断出 T。所以我们会写 {[I in keyof T]: [T[I], NoInfer<Dupe<T[I]>>]}.

TypeScript 中没有原生或官方 NoInfer<T> 类型,但有一些实现在某些情况下可以工作。我倾向于使用 this version 或类似的东西来利用编译器倾向于 延迟 未解析条件类型的评估:

type NoInfer<T> = [T][T extends any ? 0 : never];

如果您为 T 输入一些特定类型,这将计算为 T(例如,NoInfer<string>[string][string extends any ? 0 : never],即 [string][0]string),但是当 T 是未指定的泛型类型时,编译器不会对其求值并且不会急于将其替换为 T。所以当 T 是一些泛型类型参数时,NoInfer<T> 对编译器是不透明的,我们可以用它来阻止类型推断。


所以有解释(哇!)。让我们确保它有效:

g(
  ['a', 'aa'], // okay
  ['b', 'bb'], // okay
  ['c', 'dd']  // error!
  // -> ~~~~
  // Type '"dd"' is not assignable to type '"cc"'. (2322)
)

看起来不错!编译器对 ["a", "aa"]["b", "bb"] 很满意,但对 ["c", "dd"] 犹豫不决,并给出了一条很好的错误消息,说明它如何预期 "cc" 但得到了 "dd"


这回答了问题。但请注意,g() 的输入只会对 呼叫者 真正有用,他们使用 specificliteral 字符串传入。一旦有人用未解析的 generic 字符串调用 g()(比如在某些通用函数的主体内) ,或者一旦您尝试检查 g() 的实现主体内部的 Targs,您会发现编译器无法真正理解其中的含义。在这些情况下,您最好扩大到 [string, string][] 之类的范围,并且要小心。

Playground link to code