在推断泛型类型时避免扩大

Avoid widening while inferring generic types

拿这个例子函数

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 同样的例子

有没有办法使原始示例起作用?

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

首先让我将您的签名重构为一个几乎等效的版本,这样分析起来可能更简单:

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