【问题标题】:Avoid widening while inferring generic types在推断泛型类型时避免扩大
【发布时间】:2020-12-15 09:47:17
【问题描述】:

以这个函数为例

type Decoder<A, B> = (v: A) => B 

declare function test<Values, D extends Decoder<Values, unknown>>(options: {
    values: Values,
    decoder: D,
    onDecoded: (decodedValue: ReturnType<D>) => unknown
}): void;

这个想法是onDecoded 输入由decoder 计算的值。然而:

test({
    values: { a: "" },
    decoder: values => values.a.length,
    onDecoded: decodedValue => {
        decodedValue // unknown
    }
})

奇怪的是,如果我在decoder 的定义中不使用values,那么decodedValue 的类型是正确的

test({
    values: { a: "" },
    decoder: () => 42,
    onDecoded: decodedValue => {
        decodedValue // number
    }
})

这是一个playground link 与相同的例子

有没有办法让原始示例工作?

【问题讨论】:

  • 更奇怪的是(对我来说),decodedValue 的类型会更改为number,如果你只是将decoder 更改为decoder: (values) =&gt; 42。只是声明参数,甚至不使用它,改变了事情。
  • 嘿,谢谢你的问题。确实D 扩展了Decoder&lt;Values, unknown&gt;,但它的实际类型被推断为Decoder&lt;Values, number&gt;,所以我预计ReturnType&lt;D&gt; 会导致number。这不是一个公平的假设吗?
  • 是的,我对unknown 类型还很陌生,但我想我明白了。谢谢。
  • 你可以替换那里的任何东西,真的。 unknown 只是这个案例的合理上限,但理想情况下我希望它永远不会被推断出来
  • 是的,我也注意到了这种行为。似乎只是添加参数会改变类型推断算法......

标签: typescript type-inference typescript-generics


【解决方案1】:

这里的问题是编译器在推断出一切之前就放弃了。你有一个对象,编译器需要从中推断出两个类型参数,但它不能一次完成。

首先让我将您的签名重构为一个几乎等效的版本,可能更易于分析:

declare function test<A, B>(options: {
    values: A,
    decoder: (a: A) => B,
    onDecoded: (b: B) => unknown
}): void;

这与您的版本具有相同的推理问题,但谈论类型要容易一些。无论如何,编译器需要从 options 值 passend 推断出 AB,您想从中推断出 AB。它可以从values的类型推断A,但它可能无法推断B,除非decoder的实现碰巧不依赖A,所以它失败了。

类型推断的细节不是我的专家。但是,如果对这个问题有一个规范的答案,那就是microsoft/TypeScript#38872,它使用非常相似的数据结构并遇到了同样的问题。这在 TypeScript 中被归类为设计限制,因此可能无法在不更改 test 函数或调用方式的情况下解决此问题。


改变你调用它的方式将涉及向编译器提供足够的类型信息以使其工作。例如,如果你在调用decoder的输入参数的时候注释了它的类型,你就可以了:

test({
    values: { a: "" },
    decoder: (values: { a: string }) => values.a.length, // annotate
    onDecoded: decodedValues => {
        decodedValues // number
    }
})

或者您可以更改test() 的定义方式。我的一个建议是将options 对象拆分为单独的参数。与单个参数相比,编译器更愿意为不同的函数参数花费多次推理传递。可能是这样的:

declare function test2<A, B>(values: A,
    decoder: (a: A) => B,
    onDecoded: (b: B) => unknown
): void;

test2(
    { a: "" },
    values => values.a.length,
    decodedValues => {
        decodedValues // number
    }
)

test2({ a: "" },
    () => 42,
    decodedValues => {
        decodedValues // number
    }
)

这些推理完全按照您的意愿工作,如果必须,您可以使用 DReturnType 重写它们。


我猜你走哪条路取决于你。无论如何,希望有所帮助;祝你好运!

Playground link

【讨论】:

  • 感谢您的详细解释和指向 GitHub 问题的指针。实际上,我最终得到了一个非常相似的解决方法(单独的位置参数),但现在它的工作原理更加清楚了!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多