我正在尝试创建一个接受 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 推理算法的已知限制或缺失功能,如 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 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 代码链接