I have a two part question here. The first is a straightforward specific technical clarification. The second is that I have inconsistent behaviour between my VSCode and TypeScript and I want to know what setting is causing it.
I have a type that makes multiple references to a generic parameter T like:
type SomeComponentProps<T> = {
onChange: (value: T) => void;
availableValues: Array<T>;
selectedValue: T | null;
generateString: (value: T) => string;
}
And I use this type like:
SomeComponent({
onChange: (value: Foo | null | string | number) => {}, // Note all the typings here.
availableValues: foos,
selectedValue: null,
generateString: (v) => v.a //(parameter) v: Foo
})
TypeScript Playground 4.9.5
In this playground the type of v is 'Foo'. TypeScript has decided that the generic parameter T is type Foo. Question is - how is TypeScript determining that it is Foo and not Foo | null | string | number?
Property ordering, either in the type declaration, or in the use of the function do not appear to change how the type is inferred. What else is it? A union of the possible types?
Note that changing the onChange property from an arrow function to a regular function changes the behaviour of inference:
SomeComponent({
//Changed this to a regular function
onChange: function (value: Foo | null | string | number) {},
availableValues: foos,
selectedValue: null,
generateString: (v) => v.a //Property 'a' does not exist on type 'string | number | Foo'.
//Property 'a' does not exist on type 'string'.
})
TypeScript Playground
I have copy pasted the above code into a testFile.ts and a testFile.tsx. I have manually set the TypeScript version t. 4.9.5. In both cases the parameter v has the the type Foo | null | string | number - what is likely causing this? One of the compiler options perhaps?
Runnable Repro
I have been able to reproduce this issue here: https://github.com/dwjohnston/typescript-type-inference-thingy This is essentially a basic tsc --init using TypeScript 4.9.5.
It seems like the strict: false flag changes how this inference happens. Curiously enough though, this behaviour does not occur in TypeScript playground.
Question is - how is TypeScript determining that it is Foo and not Foo | null | string | number?
The problem is that you define onChange as follows:
onChange: (value: T) => void;
But later on you supply it with an arrow function like this:
onChange: (value: Foo | null | string | number) => "hello!"
Note that onChange should be a function that returns void but your arrow function returns string. If you use a regular anonymous function like this:
onChange: function(value: Foo | null | string | number) {return;}
...TypeScript will infer Foo | null | string | number as you expect:

Using an anonymous function does. But the question still remains, why does using an regular anonymous function change the behaviour of the inference compare to an arrow anonymous function ?
The Lord works in mysterious ways...and apparently so does TypeScript. It turns out, if you use a regular anonymous function that is assigned to a variable, the behaviour changes again, this time it infers Foo just like it did with an arrow function:
const fun2 = function(value: Foo | null | string | number) { return; }
SomeComponent({
onChange: fun2, // <-- anonymous function assigned via a variable. Infers Foo.
availableValues: foos,
selectedValue: null,
generateString: (v) => v.a //(parameter) v: Foo
})
I honestly wouldn't stress too much about this because the TypeScript team admits that they sometimes get these things wrong. To quote the pull request on Higher order function type inference which covers some of the logic at play here (emphasis is mine):
The above algorithm is not a complete unification algorithm and it is by no means perfect. In particular, it only delivers the desired outcome when types flow from left to right. However, this has always been the case for type argument inference in TypeScript, and it has the highly desired attribute of working well as code is being typed.
From the above we can at least know for sure that type inference works in a left-to-right manner, but beyond that, I reckon only the TypeScript team can explain the difference in behaviour between inline anonymous functions, arrow functions and a variable that's been assigned an anonymous function.
So in summary: TypeScript does work in mysterious ways...but occasionally, mysterious things like this lead to really cool things like --strictBindCallApply.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With