英文:
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
来推断T
是Property.Height
,从而获得所期望的行为。但目前还不包括在语言中。
目前,如果你想在函数内部使用if
/else
或switch
/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
不再是泛型函数,而是接受一个辨别联合类型的剩余参数,其中第二个参数辨别了参数列表。你不再可以进行错误的调用,如上所示。
英文:
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() < 0.001 ? Property.DateOfBirth : Property.Height);
// function example<Property.Height | Property.DateOfBirth>(
// 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() < 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论