Gabriele Petronella
Gabriele Petronella

Reputation: 108159

Avoid widening while inferring generic types

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

Answers (1)

jcalz
jcalz

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.

Playground link

Upvotes: 3

Related Questions