Reputation: 108159
Take this example function
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;
The idea is that onDecoded
gets in input the value computed by decoder
. However:
test({
values: { a: "" },
decoder: values => values.a.length,
onDecoded: decodedValue => {
decodedValue // unknown
}
})
Strangely enough, if I don't use values
in the definition of decoder
then decodedValue
has the correct type
test({
values: { a: "" },
decoder: () => 42,
onDecoded: decodedValue => {
decodedValue // number
}
})
Here's a playground link with the same example
Is there a way to make the original example work?
Upvotes: 2
Views: 307
Reputation: 329658
The problem here is that the compiler gives up before it can infer everything. You've got a single object from which the compiler needs to infer two type parameters, but it can't do it all at once.
First let me refactor your signature to a nearly equivalent version that might be more straightforward to analyze:
declare function test<A, B>(options: {
values: A,
decoder: (a: A) => B,
onDecoded: (b: B) => unknown
}): void;
This has the same inference issue as your version, but it's a little easier to talk about the types. Anyway, the compiler needs to infer A
and B
from the options
value passend you want to infer A
and B
from it. It can infer A
from the type of values
, but it probably can't infer B
unless the implementation of decoder
happens not to depend on A
, so it fails.
The details of type inference aren't something I'm an expert on. But if there is a canonical answer to this question, it's at microsoft/TypeScript#38872 which uses a very similar data structure and runs into the same problem. This is classified as a design limitation in TypeScript, so there's probably no way to fix this without altering your test
function or the way you call it.
Altering the way you call it would involve giving enough type info to the compiler to allow it to work. For example, If you annotate the type of decoder
's input argument when you call it, you're fine:
test({
values: { a: "" },
decoder: (values: { a: string }) => values.a.length, // annotate
onDecoded: decodedValues => {
decodedValues // number
}
})
Or you can change how test()
is defined. One suggestion I have is to split the options
object out into separate parameters. The compiler is a little more willing to spend multiple inference passes for different function parameters than it is for a single parameter. Maybe like this:
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
}
)
Those inferences work exactly as you want, and you can probably rewrite them using D
and ReturnType
if you must.
Which way you go is up to you I guess.
Upvotes: 3