为什么在这个例子中 `value` 没有自动缩小到 `number` 类型?

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

Why isn't `value` being automatically narrowed to type `number` in this example?

问题

我不明白为什么在这个 TypeScript 函数中,“value”没有被缩小为 `number`。我在想,如果 `T`  `Property.Height`,那么 `Data[T]` 应该是类型为 `Data[Property.Height]`  `number`
英文:

I don't understand why "value" isn't narrowed to number in this TypeScript function. I'm thinking, if T is Property.Height then Data[T] should be of type Data[Property.Height] which is number

enum Property {
    Height = 'height',
    Name = 'name',
    DateOfBirth = 'date_of_birth'
}

interface Data {
    [Property.Height]: number;
    [Property.Name]: string;
    [Property.DateOfBirth]: Date;
}

function example<T extends Property>(value: Data[T], property: T) {
    if (property === Property.Height) {
        // why isn't "value" narrowed to number here?
    }
}

答案1

得分: 4

TypeScript目前无法使用控制流分析来影响example函数体内的泛型类型参数T,因此,虽然检查property === Property.Height会缩小property的类型,但T类型参数本身将保持不变。

其中一个原因是T不会发生变化,因为检查property === Property.Height并不意味着T就是Property.Height约束T extends Property并不意味着"T必须是Property联合类型的一个成员",它可以是Property的任何子类型,包括Property联合类型本身。让我们看看如果我们使用联合类型调用example()会发生什么:

// 这不会导致编译错误:
example(new Date(), Math.random() < 0.001 ? Property.DateOfBirth : Property.Height);

// 函数签名变为:
// function example<Property.Height | Property.DateOfBirth>(
//    value: number | Date, property: Property.Height | Property.DateOfBirth
// ): void

这个调用是允许的,你可以看到T已经被推断为联合类型Property.Height | Property.DateOfBirth,这意味着Data[T]被推断为number | Date。在这个调用中,property === Property.Height的概率是99.9%,而value确切地Date。出现了问题。

有一个长期存在的特性请求在microsoft/TypeScript#27808上,请求使用一些其他的语法来替代T extends Property,也许像T oneof Property这样的语法,意味着T必须恰好是Property联合类型的一个成员。然后,编译器可以使用property === Property.Height来推断TProperty.Height,从而获得所期望的行为。但目前还不包括在语言中。


目前,如果你想在函数内部使用if/elseswitch/case分析,泛型并不会真正帮助你。相反,支持的方法是使用辨别联合类型。我可以将代码重构如下:

type ExampleArgs = { [K in Property]: [value: Data[K], property: K] }[Property];

function example(...[value, property]: ExampleArgs) {
  if (property === Property.Height) {
    value.toFixed(); // 可以正常工作
  }
}

// 调用示例:
example(new Date(), Property.DateOfBirth); // 正常
example(new Date(), Property.Height); // 错误

我不想偏离太多以解释它是如何计算的,但你可以看到现在example不再是泛型函数,而是接受一个辨别联合类型的剩余参数,其中第二个参数辨别了参数列表。你不再可以进行错误的调用,如上所示。

[代码示例链接](https://www.typescriptlang.org/play?noErrorTruncation=true#code/KYOwrgtgBACgTgewA7DgFwJ5QN4CgpQASwAlgOYAWaUAvFAOQWmVr0A0+UAcgIYTC0GIPsHacAIjzTAA8gDMAQiXQVB9ACZTgAfQRztAI2VoK9XAF9cuEiGlw5PAMYDJaHjk4BteMlSYAdMTkVAC6AFxQ4BAGqADcXj4o6Bj+vPzhUADOaHA2ZPEE3ohJAa6yisYUGWXxlrhyYCCOaCQIIFDAAB58SAA2wAA8ACodndIg6pmwxX4YAHwAFABuPL1gwBGuPJ5DIWxQSDPJEUMAlB4EJHJQC4e+ybQ0dImzgcxU53gEBAD0P1AAdwoWBImRA9GoACIVmtgJDIjw4IgAcB1FA0AhIpAYnAoEw4MAAPzfKCcSx1Lo9foLEDAAFQMoLU77ACyUgo-jgPAmCAgTKgAygAAZ-EKhQBGKDEl7JfxleRKFRQCIygJBFineJ-KANJotNqjKmDVUpdVUKAAH2m91KWgVlUWuG13xh6wiURxloZWn2dxKGBVRzV72oVpNcrtFRUTv+pwiSwQJHUVmE-EySCcAgAYiROh5OJgUFAAKLdCB9YAAQTgZCmdGwUE8AGkoDZrf6Mp5XRtvW5m3sDkGA1AmyEoOYijaMCFOD8AFTojBF0tG6u1wScEld1ZurHRVC+oeBqdvYJoMdWzffbewiLZXIgMiHqfH-2pEQX0kkwrdzY+wcvu2rzylGJhjpwc4-FYBC6s0rTtJS5bUv4KE3usz4dhEK5IVWNaZJ8V5XDcfqzI8zxDqeGoXN+3b+BiOadKiTKxFA2oAggcAANaZFelgEHUBCIRWNJ0r2wBMqy7Kcty6i8vygoimKkrShRIGKiYypAbKZpoJqrH-KgiBwAAhAQnBCdStL0oyzJaba0j2ioenaggnE8FgpLQfpOqNHBBoWcaFE6V64ZqQ6C

英文:

TypeScript is not currently able to use control flow analysis to affect generic type parameters like T inside the body of example. So while checking property === Property.Height will narrow the type of property, the T type parameter itself will stubbornly stay the same.

One reason T doesn't change is because it is not correct to say that checking property === Property.Height implies that T is Property.Height. The constraint T extends Property doesn't mean "T is exactly one of the union members of Property". It can be any subtype of Property, including the full Property union itself. Let's see what happens if we call example()` with union types:

// this does not cause a compiler error:
example(new Date(), Math.random() &lt; 0.001 ? Property.DateOfBirth : Property.Height); 

// function example&lt;Property.Height | Property.DateOfBirth&gt;(
//    value: number | Date, property: Property.Height | Property.DateOfBirth
// ): void

That call is allowed, and you can see that T has been inferred to be the union Property.Height | Property.DateOfBirth, which means that Data[T] is inferred to be number | Date. In that call there is a 99.9% chance that property === Property.Height while value is definitely Date. Oops.

There is a longstanding open feature request at microsoft/TypeScript#27808 asking for some other syntax instead of T extends Property.. maybe say something like T oneof Property meaning that T must be exactly one member of the Property union. And then maybe the compiler could use property === Property.Height to conclude that T is Property.Height and you'd get the behavior you want. But for now it's not part of the language.


Right now, if you want to use if/else or switch/case case analysis inside your function, generics won't really help you. Instead, the supported way to do that is with discriminated unions. I could refactor that code to look like:

type ExampleArgs = { [K in Property]: [value: Data[K], property: K] }[Property]
/* type ExampleArgs = 
  [value: number, property: Property.Height] | 
  [value: string, property: Property.Name] | 
  [value: Date, property: Property.DateOfBirth] 
*/

function example(...[value, property]: ExampleArgs) {
  if (property === Property.Height) {
    value.toFixed(); // works
  }
}


example(new Date(), 
  Math.random() &lt; 0.001 ? Property.DateOfBirth : Property.Height); // error!  
example(new Date(), Property.DateOfBirth); // okay  

I don't want to digress too much to explain how that was computed, but you can see that now example isn't generic; instead it takes a rest parameter of a discriminated union type, where the second argument discriminates the argument list. And you can't make bad calls with it anymore, as shown above.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月16日 05:20:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76485593.html
匿名

发表评论

匿名网友

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

确定