如何使用通用类型推断TypeScript数组的类型

5

代码

type A = {
  name: string
}

type B = {
  id: number
}

function foo<T extends A | B>(target: T[]): T[] {
  const res = [];
  // const res: T[] = []; // Adding type annotation to res can resolve the error but why?
  for (const e of target) {
    res.push(e);
  }
  return res // TS think res is a type of (A|B)[] --> error!
}

function aoo<T extends A>(target: T[]): T[] {
  const res = [];
  for (const e of target) {
    res.push(e);
  }
  return res // TS think res is a type of T[] --> no error!
}

TS Playground

我有两种类型A和B, 和两个泛型函数fooaoo

第一个函数foo的泛型类型 T 受到联合约束:A | B,而后者只受A约束。

错误

错误出现在foo中,原因是TS认为结果是类型(A|B)[],与T[]不兼容。然而,我预期的aoo的返回类型被推断为T[]。对我来说这很奇怪,我不理解为什么TSC不将foo的返回类型推断为T[]?这两种情况之间有什么区别?


我在这里找不到一个标准答案。这与自动类型变量有关,也称为隐式any变量。你有一个自动类型的数组,但你可以重现那些不是数组的情况,比如这个。它们是在 ms/TS#11263ms/TS#11432中实现的。当类型演化为联合约束泛型时,它被扩展到约束,但当类型演化为非联合约束泛型时,它不会被扩展。 - jcalz
我在 GitHub 上还没有找到关于这个问题的记录,但我会继续寻找。 - jcalz
看起来这是在TS4.2和TS4.3之间开始的。 - jcalz
缺失于4.3.0-dev.20210316版本,出现于4.3.0-dev.20210323版本。 - jcalz
1
找到了,它在 https://github.com/microsoft/TypeScript/pull/43183。这是为了控制流分析而引入的,并具有此副作用。我可以写出为什么会发生这种情况。 - jcalz
显示剩余2条评论
1个回答

2
这是 Typescript 4.3 中添加的 上下文缩小泛型类型值 的副作用,该特性由 microsoft/TypeScript#43183 实现。长期存在着一个开放问题 microsoft/TypeScript#13995,其中控制流分析无法像处理特定类型的值那样缩小被约束联合类型的泛型类型值。
例如,以下代码总是有效的,其中x是特定联合类型string | number的变量:
function checkSpecific(x: string | number) {
  if (typeof x !== "string") {
    console.log(x.toFixed(2)); // okay
  }
}

这里的事实是,typeof x !== "string" 允许编译器将 x 缩小为 number 并看到它具有 toFixed() 方法。但在 TypeScript 4.3 之前,下面的代码不起作用,其中 x 的类型为 T extends string | number
function checkGeneric<T extends string | number>(x: T) {
  if (typeof x !== "string") {
    console.log(x.toFixed(2)); // error (before TS4.3)! 
    // ---------> ~~~~~~~
    //  Property 'toFixed' does not exist on type 'T'.
  }
}

编译器顽固地拒绝看到typeof x !== "string"x的类型有任何影响。编译器不能假设类型参数T本身应该被缩小。毕竟,也许T确实是完整的联合类型string | number(例如checkGeneric(Math.random()<0.5 ? "abc" : 123)),所以缩小T不正确。但是编写上述代码的人并不关心缩小T,他们希望将xT缩小为number

因此,使用TypeScript 4.3,在某些情况下,当给出泛型类型的值时,其中泛型类型参数被限制为联合类型,则这些值将首先扩展到约束,并且然后可以进行缩小:

function checkGeneric<T extends string | number>(x: T) {
  if (typeof x !== "string") {    
    console.log(x.toFixed(2)); // okay (TS4.3 and above)
  }
}

编译器决定将 x 的类型视为特定的类型 string | number,而不是泛型类型 T。一旦这样做了,typeof x !== "string" 就可以将 x 缩小为所需的 number
不幸的是,在你的代码中,相同的分析会导致意想不到的行为。在 TypeScript 4.3 之前,不会出现错误:
function foo<T extends A | B>(target: T[]): T[] {
  const res = []; // <-- auto typed
  for (const e of target) {
    res.push(e); // <-- res is inferred as T[] before TS4.3
  }
  return res // okay
}

变量 res 被认为是“自动类型”或“隐式 any”变量,因为编译器无法使用其初始化程序推断类型;它需要等待看看你对它做了什么,然后根据此“演化”类型。对于像 res 这样的数组,这是在 microsoft/TypeScript#11432 中实现的。
在 TypeScript 4.3 之前,当你调用 res.push(e) 时,编译器会看到 e 的类型为 T,因此 res 现在演化为类型 T[],然后 return res 就可以了。
但从 TypeScript 4.3 开始,这种情况已经改变:
function foo<T extends A | B>(target: T[]): T[] {
  const res = []; // <-- still auto typed
  for (const e of target) {
    res.push(e); // <-- res is inferred as (A | B)[] starting with TS4.3
  }
  return res // error!
}

变量res仍然是自动类型的,并且在调用res.push(e)时进行演化。但是由于值e是一个通用类型,受到联合约束,编译器使用其新的行为,将eT扩展到限制范围内的A | B。这意味着res的类型是(A | B)[],并且您会收到错误消息。由于您在此代码中从未尝试将eA | B缩小为AB,因此控制流分析的增强支持对您的目的完全没有用处。

哦,好吧。


请注意,旧行为和新行为都不是“错误”的;类型为 TT extends XXX 的值可以安全地扩展为 XXX。只是在某些情况下,TXXX 更或更少有用。TypeScript 4.3 中添加的启发式算法改善了许多情况,但不幸的是在其他情况下使情况变得更糟。如果有人提出了这个问题,我会很感兴趣看看会发生什么,但我不会把它称为一个 bug。

Playground链接到代码pre-ms/TS#43183

(注:该文本包含一个指向某个网站的链接,具体内容需要根据上下文判断翻译)
这句话的意思是“

Playground link to code post-ms/TS#43183

”,其中包含一个链接,指向代码发布的游乐场。具体内容无法确定,需要上下文来进一步理解。

谢谢,@jcalz!这解决了我的困惑,非常感谢你提供详细的答案和相关资料! - Bo Li

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接