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

Nat*_*sha 8 typing typescript typescript-typings

请看下面的小演示:

\n
    \n
  • 函数f应该接受一对 [\'stringX\', \'stringXstringX\'] 形式(-> 这是有效的)
  • \n
  • 但函数g应该接受可变数量的此模式对。\n我如何输入函数g才能实现这一点?
  • \n
\n
function f<T extends string, U extends `${T}${T}`>(arg: [T, U]) {}\nfunction g(...args: ???[]) {} // what\'s the right type here (-> ???)?\n\nf([\'a\', \'aa\']) // ok\nf([\'a\', \'ab\']) // error as expected\n\ng(\n  [\'a\', \'aa\'], // should be okay\n  [\'b\', \'bb\'], // should be okay\n  [\'c\', \'ab\']  // should result in a compile error!!!\n)\n
Run Code Online (Sandbox Code Playgroud)\n

\xc2\xbb TypeScript 游乐场

\n

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

\n

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

\n

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

\n
// expected error at the \'ab\'\ng([\'a\', \'aa\'])([\'b\', \'bb\'])([\'c\', \'ab\'])\n\n
Run Code Online (Sandbox Code Playgroud)\n

参见演示

\n

jca*_*alz 5

我的方法如下:

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];
Run Code Online (Sandbox Code Playgroud)

这个想法是,类型参数g()通用的T[]元组类型仅由来自每对字符串的第一个元素的字符串文字类型组成args。所以如果你调用g(["a", "aa"], ["b", "bb"], ["c", "cc"]),那么T应该是元组类型["a", "b", "c"]

然后,的类型可以通过映射元组类型args来表示。对于元组中的每个数字索引, 的相应元素应该具有类似 的类型,其中以所需的方式将字符串连接到自身。所以如果是,那么 的类型应该是。TITargs[T[I], Dupe<T[I]>]Dupe<T>T["z", "y"]args[["z", "zz"], ["y", "yy"]]

这里的目标是编译器应该看到类似的调用g(["x", "xx"], ["y", "oops"])并从to be的类型推断Targs["x", "y"],然后检查 的类型args并注意到 while["x", "xx"]很好,["y", "oops"]但与预期不匹配["y", "yy"],因此出现错误。

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


首先, microsoft/TypeScript#27995上有一个未解决的问题,导致编译器无法识别T[I]肯定是string映射元组类型内部的类型{[I in keyof T]: [T[I], Dupe<T[I]>]}。即使映射的元组类型创建了另一个元组类型,编译器也会认为它可能是类似orI的其他键,因此可能是函数类型 或等。any[]"map""length"T[I]number

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

type Dupe<T extends string> = `${T}${T}`;
Run Code Online (Sandbox Code Playgroud)

就会出现一个Dupe<T[I]>会导致错误的问题。为了避免这个问题,我定义了Dupe<T>它不要求它T是一个string. 相反,它只是使用条件类型来表示“ if T is a stringthen Dupe<T> will be `${T}${T}`”:

type Dupe<T> = T extends string ? `${T}${T}` : never;
Run Code Online (Sandbox Code Playgroud)

这样就解决了这个问题。


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

也就是说,当T从推断时[["y", "zz"]],编译器将"y"再次匹配T[I]"zz"针对Dupe<T[I]>,最终推断出 的联合类型 。实际上,这非常聪明,因为如果确实是,那么 的类型将是类型,并且值与其匹配。好的!除非你想拒绝["y" | "z"]TT["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>类型,但有一些实现可以在某些情况下工作。我倾向于使用这个版本或类似的版本来利用编译器推迟对未解析的条件类型进行评估的倾向:

type NoInfer<T> = [T][T extends any ? 0 : never];
Run Code Online (Sandbox Code Playgroud)

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


这就是解释(哇!)。让我们确保它有效:

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

看起来不错!编译器对["a", "aa"]and很满意["b", "bb"],但对一条关于它预期如何但结果如何的["c", "dd"]漂亮错误消息犹豫不决。"cc""dd"


这回答了所提出的问题。但请注意,键入 for仅对传入特定字符串和文字字符串的调用者g()有用。一旦有人使用未解析的通用字符串进行调用(例如在某个通用函数体内),或者一旦你尝试检查或内部的实现,你会发现编译器无法真正理解其中的含义。在这种情况下,你最好将范围扩大到类似的东西,并且要小心。g()g()Targsg()[string, string][]

Playground 代码链接