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.
1. Which property does/should TypeScript use to determine the generic parameter?
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
})
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'.
})
2. Inconsistent behaviour in between TypeScript Playground and VSCode
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.
3
Answers
I don’t know anything about the way TS infers the type argument, probably only TS devs know it. Maybe it’s trying to find the most specific type that fits, but I’m not sure. However there is a trick to tell typescript exactly from which place it should infer the type, which you may find helpful. Here’s what it looks like:
Here only in
onChange
theT
is not wrapped inNoInfer
, so its value will be inferred only from there. Playground link.The problem is that you define
onChange
as follows:But later on you supply it with an arrow function like this:
Note that
onChange
should be a function that returnsvoid
but your arrow function returnsstring
. If you use a regular anonymous function like this:…TypeScript will infer
Foo | null | string | number
as you expect:UPDATE (to answer question from comment)
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: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):
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.
TypeScript infers the generic parameter T in the type SomeComponentProps based on the type of the value passed in for the availableValues property. In your example, foos is an array of type Foo[], so TypeScript infers that T is the same as Foo. This is because the Array type in the SomeComponentProps type definition requires that the availableValues property is an array of the same type as T.
The type of v in the generateString function is inferred based on the type of the selectedValue property, which is of type T | null. Since you passed null as the value for selectedValue, TypeScript infers that T must be Foo | null.
The behavior you’re seeing in VSCode is likely due to the version of TypeScript being used by VSCode, which may be different from the version you’re using in the TypeScript Playground. You can check the version of TypeScript being used by VSCode by opening the command palette (Ctrl+Shift+P on Windows or Linux, Cmd+Shift+P on macOS) and searching for "TypeScript: Select TypeScript Version". This will show you the version of TypeScript that VSCode is currently using.
If you want to use the same version of TypeScript in VSCode as you’re using in the TypeScript Playground, you can configure VSCode to use a specific version of TypeScript by adding a tsconfig.json file to your project and specifying the version of TypeScript you want to use in the compilerOptions section. For example, to use TypeScript version 4.9.5, you could add the following to your tsconfig.json file:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"isolatedModules": true,
"types": [],
"lib": ["es6"],
"types": ["node"],
"typescript": "4.9.5"
},
"include": ["src/**/*"]
}
This will tell VSCode to use TypeScript version 4.9.5 when compiling your project.
The reason for the difference in behavior between the arrow function and the regular function in your example has to do with how TypeScript infers the type of the onChange property in the SomeComponent function call.
When you use an arrow function, TypeScript infers the type of the onChange property as a union type of all the possible types of the arrow function parameters. In your case, the arrow function has a parameter of type Foo | null | string | number, so TypeScript infers the type of the onChange property as ((value: Foo | null | string | number) => void).
On the other hand, when you use a regular function, TypeScript infers the type of the onChange property as the type of the function itself. In your case, the type of the function is (value: Foo | null | string | number) => void, so TypeScript infers the type of the onChange property as (value: Foo | null | string | number) => void.
The difference in behavior between the two is due to the fact that arrow functions have a more complex syntax that allows them to represent multiple types of functions, including functions with optional or rest parameters. This makes it more difficult for TypeScript to infer the type of the arrow function, and so it falls back to inferring the union type of all the possible parameter types.
In contrast, regular functions have a simpler syntax that only allows them to represent a single type of function, so TypeScript can more easily infer the type of the function.
The strict compiler option in TypeScript enables a set of strict type-checking options, including –strictNullChecks, which is responsible for the difference in behavior you’re seeing between strict and non-strict mode.
In non-strict mode, TypeScript allows null and undefined to be assignable to any type, which means that the type of the selectedValue property in your example is inferred as T | null, where T is inferred as Foo. This means that the type of the generateString function is inferred as (value: Foo | null) => string.
However, in strict mode, TypeScript enforces stricter type checking rules, including the –strictNullChecks option, which prevents null and undefined from being assignable to non-nullable types. This means that the type of the selectedValue property is inferred as T, where T is inferred as Foo | null. This means that the type of the generateString function is inferred as (value: Foo | null) => string in non-strict mode and as (value: Foo) => string in strict mode.
The reason for the difference in behavior between VSCode and the TypeScript Playground may be due to differences in the versions of TypeScript being used or in the configuration of the TypeScript environment. It’s possible that the TypeScript Playground is using a different version of TypeScript or a different configuration than your local environment, which could be causing the difference in behavior.