skip to Main Content

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
})

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

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


  1. 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:

    // Add this weird type
    type NoInfer<T> = T extends any ? T : T
    
    type SomeComponentProps<T> = {
      onChange: (value: T) => void; 
      // Everywhere you don't want the generic to be inferred from, wrap it into this type
      availableValues: Array<NoInfer<T>>; 
      selectedValue: NoInfer<T> | null; 
      generateString: (value: NoInfer<T>) => string; 
    }
    

    Here only in onChange the T is not wrapped in NoInfer, so its value will be inferred only from there. Playground link.

    Login or Signup to reply.
  2. 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:

    enter image description here

    UPDATE (to answer question from comment)

    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.

    Login or Signup to reply.
    1. 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.

    2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search