TypeScript 如何推断其泛型参数?

huangapple go评论47阅读模式
英文:

How does TypeScript infer its generic parameter?

问题

1. TypeScript使用哪个属性来确定泛型参数?

我有一个类型,其中多次引用了泛型参数T,如下所示:

type SomeComponentProps<T> = {
  onChange: (value: T) => void; 
  availableValues: Array<T>; 
  selectedValue: T | null; 
  generateString: (value: T) => string; 
}

我像这样使用这个类型:

SomeComponent({
  onChange: (value: Foo | null | string | number) => {}, // 请注意这里的所有类型。
  availableValues: foos,
  selectedValue: null,
  generateString: (v) => v.a //(参数) v: Foo
})

在这个播放器中,v的类型是'Foo'。 TypeScript已经确定泛型参数T是类型Foo。问题是 - TypeScript是如何确定它是Foo而不是Foo | null | string | number的?

属性的顺序,无论是在类型声明中还是在函数的使用中,似乎都不会改变类型的推断方式。还有其他什么因素?可能是可能类型的联合吗?

请注意,将onChange属性从箭头函数更改为普通函数会改变推断的行为:

SomeComponent({
  //将这个更改为普通函数
  onChange: function (value: Foo | null | string | number) {},
  availableValues: foos,
  selectedValue: null,
  generateString: (v) => v.a //属性'a'在类型'string | number | Foo'上不存在。
                                //属性'a'在类型'string'上不存在。
})

2. TypeScript Playground和VSCode之间的不一致行为

我已经将上述代码复制粘贴到了testFile.tstestFile.tsx中。我手动设置了TypeScript版本为4.9.5。在这两种情况下,参数v的类型为Foo | null | string | number - 可能是什么导致了这种情况?也许是其中一个编译器选项?

可运行的复现

我已经在这里复现了这个问题:https://github.com/dwjohnston/typescript-type-inference-thingy 这基本上是一个使用TypeScript 4.9.5的基本tsc --init

看起来strict: false标志会改变推断发生的方式。但有趣的是,在TypeScript Playground中不会出现这种情况。

英文:

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&lt;T&gt; = {
  onChange: (value: T) =&gt; void; 
  availableValues: Array&lt;T&gt;; 
  selectedValue: T | null; 
  generateString: (value: T) =&gt; string; 
}

And I use this type like:

SomeComponent({
  onChange: (value: Foo | null | string | number) =&gt; {}, // Note all the typings here.
  availableValues: foos,
  selectedValue: null,
  generateString: (v) =&gt; 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) =&gt; v.a //Property &#39;a&#39; does not exist on type &#39;string | number | Foo&#39;.
                                //Property &#39;a&#39; does not exist on type &#39;string&#39;.
})

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.

答案1

得分: 2

问题是 - TypeScript 如何确定它是 Foo,而不是 Foo | null | string | number?

问题出在你如下定义了 onChange

onChange: (value: T) => void;

但后来你却用箭头函数提供了它,像这样:

onChange: (value: Foo | null | string | number) => "hello!"

注意,onChange 应该是一个返回 void 的函数,但你的箭头函数返回了 string。如果你使用一个常规的匿名函数,像这样:

onChange: function(value: Foo | null | string | number) {return;}

...TypeScript 将会推断为你期望的 Foo | null | string | number

更新(回答评论中的问题)

使用匿名函数确实有效。但问题仍然是,为什么使用常规匿名函数与箭头匿名函数相比会改变推断的行为呢?

主要原因是 TypeScript 的类型推断是基于代码流动的方式来进行的,而不是函数类型的定义方式。所以当你使用常规匿名函数时,TypeScript 能够更准确地推断类型,因为它能够看到函数内部的代码流动。当你使用箭头匿名函数时,由于它是一个表达式,TypeScript 推断会受到限制,因为它无法像常规函数那样检查函数内部的代码流动。所以,使用常规匿名函数会导致更准确的类型推断。

总之,TypeScript 的类型推断是复杂的,有时候会出现一些奇怪的行为,但通常情况下,使用常规匿名函数会获得更精确的类型推断。

英文:

> 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) =&gt; void; 

But later on you supply it with an arrow function like this:

onChange: (value: Foo | null | string | number) =&gt; &quot;hello!&quot;

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:

TypeScript 如何推断其泛型参数?

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, // &lt;-- anonymous function assigned via a variable. Infers Foo.
  availableValues: foos, 
  selectedValue: null,   
  generateString: (v) =&gt; 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.

答案2

得分: 0

  1. TypeScript 根据传递给 availableValues 属性的值的类型,推断 SomeComponentProps<T> 类型中的泛型参数 T。在你的示例中,foos 是一个类型为 Foo[] 的数组,因此 TypeScript 推断 T 的类型为 Foo。这是因为 SomeComponentProps<T> 类型定义中的 Array<T> 类型要求 availableValues 属性的类型与 T 相同,也就是 Foo 类型。

在 generateString 函数中,变量 v 的类型是基于 selectedValue 属性的类型推断的,而 selectedValue 的类型是 T | null。由于你传递了 null 作为 selectedValue 的值,TypeScript 推断 T 必须是 Foo | null。

  1. 在 VSCode 中观察到的行为差异可能是由于所使用的 TypeScript 版本不同引起的,这可能与你在 TypeScript Playground 中使用的版本不同。你可以通过在 VSCode 中打开命令面板(在 Windows 或 Linux 上按 Ctrl+Shift+P,在 macOS 上按 Cmd+Shift+P),然后搜索 "TypeScript: Select TypeScript Version" 来查看 VSCode 当前使用的 TypeScript 版本。

如果你想在 VSCode 中使用与 TypeScript Playground 中相同的 TypeScript 版本,可以通过在项目中添加一个 tsconfig.json 文件并在 compilerOptions 部分指定要使用的 TypeScript 版本来配置 VSCode。例如,要使用 TypeScript 版本 4.9.5,你可以将以下内容添加到你的 tsconfig.json 文件中:

{
  "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/**/*"]
}

这将告诉 VSCode 在编译项目时使用 TypeScript 版本 4.9.5。

在你的示例中,箭头函数和常规函数之间的行为差异与 TypeScript 推断 SomeComponent 函数调用中 onChange 属性的类型有关。

当使用箭头函数时,TypeScript 推断 onChange 属性的类型为箭头函数参数的所有可能类型的联合类型。在你的示例中,箭头函数具有类型 Foo | null | string | number,因此 TypeScript 推断 onChange 属性的类型为 ((value: Foo | null | string | number) => void)。

另一方面,当使用常规函数时,TypeScript 推断 onChange 属性的类型为函数本身的类型。在你的示例中,函数的类型是 (value: Foo | null | string | number) => void,因此 TypeScript 推断 onChange 属性的类型为 (value: Foo | null | string | number) => void。

这两者之间的行为差异是因为箭头函数具有更复杂的语法,允许它们表示多种类型的函数,包括具有可选或剩余参数的函数类型。这使得 TypeScript 更难推断箭头函数的类型,因此它退而推断所有可能参数类型的联合类型。

相比之下,常规函数具有更简单的语法,只允许它们表示单一类型的函数,因此 TypeScript 更容易推断函数的类型。

TypeScript 中的 strict 编译选项启用了一组严格的类型检查选项,包括 --strictNullChecks,这是导致你在严格模式和非严格模式之间看到的行为差异的原因。

在非严格模式下,TypeScript 允许将 null 和 undefined 分配给任何类型,这意味着在你的示例中,selectedValue 属性的类型被推断为 T | null,其中 T 被推断为 Foo。这意味着 generateString 函数的类型被推断为 (value: Foo | null) => string。

然而,在严格模式下,TypeScript 强制执行更严格的类型检查规则,包括 --strictNullChecks 选项,阻止将 null 和 undefined 分配给不可为空类型。这意味着 selectedValue 属性的类型被推断为 T,其中 T 被推断为 Foo | null。这意味着在非严格模式下,generateString 函数的类型被推断为 (value: Foo | null) => string,而在严格模式下,它被推断为 (value: Foo) => string。

VSCode 和 TypeScript Playground 之间的行为差异可能是由于所使用的 TypeScript 版本或 TypeScript 环境配置的不同而引起的。可能 TypeScript Playground 使用了不同于你本地环境的 TypeScript 版本或不同的配置,这可能导致行为上的差异。

英文:
  1. TypeScript infers the generic parameter T in the type SomeComponentProps<T> 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<T> type in the SomeComponentProps<T> 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.

答案3

得分: -1

我不清楚TS是如何推断类型参数的,可能只有TS的开发人员知道。也许它试图找到最适合的具体类型,但我不确定。不过有一个小技巧,可以告诉TypeScript应该从哪个地方推断类型,你可能会找到它有帮助。下面是示例代码:

// 添加这个奇怪的类型
type NoInfer<T> = T extends any ? T : T

type SomeComponentProps<T> = {
  onChange: (value: T) => void; 
  // 在你不希望泛型被推断的地方,将它包装在这个类型中
  availableValues: Array<NoInfer<T>>; 
  selectedValue: NoInfer<T> | null; 
  generateString: (value: NoInfer<T>) => string; 
}

在这里,只有在 onChange 中,T 没有被包装在 NoInfer 中,因此它的值只会从那里推断。 Playground 链接

英文:

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&lt;T&gt; = T extends any ? T : T

type SomeComponentProps&lt;T&gt; = {
  onChange: (value: T) =&gt; void; 
  // Everywhere you don&#39;t want the generic to be inferred from, wrap it into this type
  availableValues: Array&lt;NoInfer&lt;T&gt;&gt;; 
  selectedValue: NoInfer&lt;T&gt; | null; 
  generateString: (value: NoInfer&lt;T&gt;) =&gt; string; 
}

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

huangapple
  • 本文由 发表于 2023年2月8日 08:57:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75380438.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定