如果包含使用泛型类型的属性,为什么打字稿会以不同的方式推断泛型类型?

问题描述 投票:0回答:1

我正在尝试创建一个接受 props 对象的函数,其中一个属性的返回类型用作另一个属性的参数的约束。

考虑以下代码:


// A base type and another type that extends from it

interface Base { x: string; }
interface Foo extends Base { y: number; }

// A fn type that accepts Foos as an argument

type Fn<T> = (t: T) => void
declare const FooFn: Fn<Foo>

// Now, I want to use the return type of 'x' to constrain the type of fn I can assign to 'y' contravariantly

type Baz = <R extends Base, F extends Fn<R>>(arg: {
  x: (bar: Foo) => R
  y?: F[]
}) => R;

// Observe that if I try to set 'y', FooFn emits an error
// Even though R is inferred from 'x' correctly (see R1)

declare const baz: Baz;
const R1 = baz({ x: (ctx) => ctx })
const R2 = baz({ x: (ctx) => ctx, y: [FooFn] }) 

根据是否声明“y”,推断“x”的返回类型有所不同。

NoInfer
中使用
F
也没有帮助。

为什么声明 'y' 时返回类型为

Context
,而省略 'y' 时返回类型为
MyContext

要明确的是,我的目标是能够将“y”设置为可以接受“x”返回类型的 fns 数组。

我也把它放在 TS 游乐场中: https://tsplay.dev/mxpM7m

typescript
1个回答
0
投票

这是 TypeScript 推理算法的已知限制或缺失功能,如 microsoft/TypeScript#47599 中所述。当调用generic函数而不手动指定类型参数时,TypeScript 会尝试推断它们。当编写回调函数而不手动注释其参数时,TypeScript 会尝试从上下文推断它们。当您的通用函数调用还包含未注释的回调时,TypeScript 必须同时推断这两种类型,那么如果这些类型以任何方式相互依赖,则推断很可能会失败。对于人类来说,如何分析代码可能是显而易见的,这样在每一步你只需要已有的信息,但 TypeScript 的启发式算法以特定的顺序发生,如果它最终需要一些东西,那么它就不会能够推断,稍后您会遇到问题。

如果您阅读 microsoft/TypeScript#47599,您会发现推理是否成功取决于您可能认为不会产生任何影响的事情。所以标题中问题的答案是:类型参数和回调参数的同时推断是

脆弱,并且以你可能意想不到的方式敏感。


就你而言,它看起来像

const R1 = baz({ x: (ctx) => ctx })
call,TypeScript 根本无法推断出 

F

,所以它甚至不会尝试。相反,它只关注
R
。它需要先知道 
ctx
 的类型,然后才能知道 
R
,因此当它进入推断上下文参数类型的推理阶段时,
ctx
 获取类型 
Foo
,然后 
R
 是根据需要推断为 
Foo
F
 只是回落到其 
约束,即 Fn<R>
,因此 
Fn<Foo>

但是随着

const R2 = baz({ x: (ctx) => ctx, y: [FooFn] })
现在 TypeScript 尝试从 

F

 的类型推断 
FooFn
。它需要在没有推断 
R
 的情况下执行此操作,因为 
R
 只能从 
x
 的返回类型推断出来(不,约束 
F extends Fn<R>
 确实 
not 帮助编译器推断 R
,请参阅 
microsoft/TypeScript#7234)。但在这个阶段,TypeScript 不知道 ctx
 是什么,因为它还没有尝试上下文输入。所以 TypeScript 所知道的关于 
R
 的一切就是它被限制为 
Base
。因此,TypeScript 假设 
R
 只是 
Base
,以约束 
F
。然后事情就会爆炸,因为 
FooFn
 无法分配给 
Fn<Base>
。一切都会回到它的约束,推理失败。

这就是帖子正文中问题的答案。


“一般”问题几乎肯定会始终存在于 TypeScript 中,但偶尔也会有一些改进,例如

TypeScript 4.7 中添加的支持(在 microsoft/TypeScript#48538 中实现)。也许有一天你上面的代码将开始按原样工作。与此同时,您需要解决它。 如果您希望 R

可以从

FooFn

 推断出来,那么您可以使用 microsoft/TypeScript#7234 中的建议来使用 
intersections
 而不是约束。也许是这样的:
type Baz = <R extends Base, F extends Fn<R>>(arg: { x: (bar: Foo) => R y?: (Fn<R> & F)[] }) => R; const R2 = baz({ x: (ctx) => ctx, y: [FooFn] }) // okay

这或多或少本质上是同一件事(尽管函数的交集很奇怪),并且推理现在会成功,因为
y
包含足够的信息来推断

R

...只要 
y 的元素
 已经有已知的函数参数类型。如果您不放 
FooFn
,而是放另一个 
xxx => xxx
,则所有赌注均无效。
如果一切都失败了,只需注释你的函数参数:

const R2 = baz({ x: (ctx: Foo) => ctx, y: [FooFn] });

是的,编译器“应该”为你做这件事。事实并非如此;你需要帮忙。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.