【问题标题】:How TypeScript infers callbacks arguments typeTypeScript 如何推断回调参数类型
【发布时间】:2021-04-09 12:09:12
【问题描述】:

我的问题是基于这个question and answer

假设我们有下一个代码:

const myFn = <T,>(p: {
  a: (n: number) => T,
  b: (o: T) => void,
}) => {
  // ...
}


myFn({
  a: () => ({ n: 0 }), // Parameter of a is ignored
  b: o => { o.n }, // Works!
})


myFn({
  a: i => ({ n: 0 }), // Parameter i is used
  b: o => { o.n }, // Error at o: Object is of type 'unknown'.ts(2571)
})

我能够修复它。它适用于额外的通用:

const myFn = <T,>(p: {
  a: (n: number) => T,
  b: <U extends T /* EXTRA U generic */>(o: U) => void,
}) => {
  // ...
}

我已经直观地修复了它。我无法解释为什么会发生这个错误,更不能解释为什么我的解决方案有帮助:)

我相信答案在协变/逆变/不变/双变定义中。

您能否解释一下为什么会出现这个错误以及为什么我的解决方案有效?

谢谢

【问题讨论】:

  • 您可以显式添加参数类型,而无需额外的泛型,a: (i: number) =&gt; ({ n: 0 }), // Parameter i is used
  • @ABOS 好点

标签: typescript typescript-generics


【解决方案1】:

常用术语

Contextual type:类型,代码位置必须具有基于它分配给的周围类型。

Context-sensitive function:带有无类型参数的函数表达式。参数类型派生自上下文类型。

示例

type Fn = (s: string) => number
const fn : Fn = s => s.length 
// `s => s.length` is context-sensitive function
// contextual type of `s` is `string`, obtained from `Fn`

类型推断的工作原理

编译器在两个阶段进行类型推断:

  • 首先它推断所有非上下文相关的函数。

  • 在第二阶段,类型参数(如T)的推断结果随后用于推断上下文相关函数。

如果第 1 阶段没有要在第 2 阶段使用的推理候选者,您可能会遇到类型问题 - 在有问题的情况下,T 获取类型 unknown

转移到您的案件

案例一:

myFn({
  a: () => ({ n: 0 }), 
  b: o => { o.n }
})

b 是上下文相关的,a 不是(无参数函数)。因此,在第一阶段,我们可以分析a 并推断T{ n: number },然后可以使用它在b 中为第二阶段键入参数o。这里一切正常。

案例 2:

myFn({
  a: i => ({ n: 0 }), // Parameter i is used
  b: o => { o.n }, // Error at o: Object is of type 'unknown'.ts(2571)
})

这里是魔鬼。 ab 都是上下文相关的,因此我们需要跳过阶段 1 - T 没有推理候选。这两个函数现在在第 2 阶段彼此独立进行分析。这是b 中的参数类型的问题,因为我们不能再咨询a,推断类型T 有什么。

唯一的方法是从基本约束推断unknown - 更糟糕的是:T 的上下文类型得到“固定”,因为阶段 2 的内部 TypeScript 算法此时需要有一个具体的类型实例化由于某些限制/优化,T。因此,对于T,我们不可逆转地使用unknown

案例 3:

const myFn = <T,>(p: {
  a: (n: number) => T,
  b: <U extends T /* EXTRA U generic */>(o: U) => void,
}) => {
  // ...
}

有效,因为您引入了一个全新的类型参数U,并做出了新的推论,独立于T。这是一个聪明的解决方法,虽然我没有测试过,如果这种方法有任何缺点。

替代解决方案

为了解决带有类型推断的令人讨厌的边缘情况,只需通过显式键入函数参数来确保至少一个具有类型参数的函数上下文相关。

例如,在a 中输入n: number 就足够了:

myFn({
  a: (n: number) => ({ n: 0 }),  // n explicitly typed
  b: o => { o.n }, // works again
})

Playground code

【讨论】:

  • 注意:这是据我所知。信息在 github 问题和博客中很少传播,因此如果您不同意其中任何一点,请随意评论/批评。
  • 非常感谢。这是非常好的答案。你对 TS 编译器有很深的了解!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-05
  • 2021-04-28
  • 2021-11-08
  • 2020-01-23
  • 2022-12-18
  • 2018-12-13
  • 2020-03-11
相关资源
最近更新 更多