让我们想象一下,我们有以下定义,我不明白为什么TypeScript仍然不能正确推断类型!
有人知道怎么正确写吗?
注意:
*确保打开“严格空检查”选项。
*我评论了解释问题的代码,如果不清楚,请发表评论。
type Diff<T, U> = T extends U ? never : T;
type NotNullable<T> = Diff<T, null | undefined>;
type OptionType<T> = T extends NotNullable<T> ? 'some' : 'none';
interface OptionValue<T> {
option: OptionType<T>;
value: T;
}
let someType: OptionType<string>; // evaludates to 'some' correctly
let noneType: OptionType<undefined>; // evaluates to 'none' correctly
let optionSomeValue = { option: 'some', value: 'okay' } as OptionValue<string>; // evaluates correctly
let optionNoneValue = { option: 'none', value: null } as OptionValue<null>; // evaluates correctly
let getValue = <T>(value: T): (T extends NotNullable<T> ? OptionValue<T> : OptionValue<never>) =>
({ option: value ? 'some' as 'some' : 'none' as 'none', value });
let handleSomeValue = <T>(obj: OptionValue<T>) => {
switch (obj.option) {
case 'some':
return obj.value;
default:
return 'empty' as 'empty';
}
}
let someStringValue = 'check'; // type string
let someNumberValue = 22;
let someUndefinedValue: string | null | undefined = undefined;
let result1 = handleSomeValue(getValue(someStringValue)); // it is 'string' correctly
let result2 = handleSomeValue(getValue(someNumberValue)); // should be 'number' but it's 'number | empty'
let result3 = handleSomeValue(getValue(someUndefinedValue)); // it is 'empty' correctly;
答案 0 :(得分:1)
这里有很多要解压的内容,但简短的版本是您需要使用显式类型注释才能使其正常工作,推理有其局限性。
有趣的是,为什么这在某些情况下显然可以按您预期的那样工作。
首先,handleSomeValue
的推断签名为<T>(obj: OptionValue<T>) => T | "empty"
。请注意,T
与返回类型中是否包含'empty'
之间没有任何关系,结果始终为T | "empty"
。那么为什么有时'empty'
丢失而有时T
丢失。嗯,这与如何评估工会的规则有关。
让我们考虑第一个例子
let someStringValue = 'check'; // type string
let result1 = handleSomeValue(getValue(someStringValue));
此处T
至handleSomeValue
将是string
,因此结果将是string | 'empty'
,但是"empty"
是string
的子类型字符串会吃掉文字类型"empty"
(因为它是多余的),结果将是string
现在让我们看一下第三个示例,该示例似乎也起作用:
let someUndefinedValue: string | null | undefined = undefined;
let result3 = handleSomeValue(getValue(someUndefinedValue)); // it is 'empty' correctly;
虽然这里的someUndefinedValue
似乎键入为string | null | undefined
,但实际上不是,如果将鼠标悬停在第二行中的someUndefinedValue
上,您会看到它的键入为{{1} }。这是因为流分析确定实际类型为undefined
,因为变量没有路径为undefined
。
这意味着undefined
将返回getValue(someUndefinedValue)
,因此OptionValue<never>
中的T
将是handleSomeValue
,因此我们得到never
。而且由于never | 'empty'
是所有类型的子类型(请参见PR),never
的求值结果仅为never | 'empty'
。
有趣的是,当'empty'
实际上是someUndefinedValue
时,该示例未能编译,因为string | undefined
将返回getValue
并且编译器将无法推断{{ 1}}。
'OptionValue<string> | OptionValue<never>'
有了这样的理解,很明显第二个示例为何无法按预期工作。
T
let someUndefinedValue: string | null | undefined = Math.random() > 0.5 ? "" : undefined;
let result3 = handleSomeValue<string | never>(getValue(someUndefinedValue)); // Argument of type 'OptionValue<string> | OptionValue<never>' is not assignable to parameter of type 'OptionValue<string>'
返回let someNumberValue = 22;
let result2 = handleSomeValue(getValue(someNumberValue)); // should be 'number' but it's 'number | empty'
,因此getValue
中的OptionValue<number>
将是T
,结果将是handleSomeValue
。由于联合中的这两种类型没有关系,因此编译器将不再尝试进一步简化联合,并将结果类型保持不变。
因为number
的并集将始终为number | 'empty'
,所以无法像您期望的那样工作并保留'empty'
文字类型的解决方案。如果我们使用品牌类型向string | 'empty'
添加一些内容以防止简化,则可以避免简化。此外,我们将需要为返回类型使用显式类型批注,以正确识别返回类型:
string
答案 1 :(得分:0)
推断出的返回类型handleSomeValue
是所有返回表达式的类型的并集,即T | "empty"
。在每个呼叫站点,此返回类型都以T
的type参数实例化。 TypeScript不会针对每个调用重新分析handleSomeValue
的主体,以查看取决于T
的切换情况。如果需要,您可以这样注释handleSomeValue
:
type SomeValueReturn<T> = T extends NotNullable<T> ? T : "empty";
let handleSomeValue = <T>(obj: OptionValue<T>): SomeValueReturn<T> => {
switch (obj.option) {
case 'some':
return obj.value as SomeValueReturn<T>;
default:
return 'empty' as SomeValueReturn<T>;
}
}
但我不太了解您要达到的目标。
答案 2 :(得分:0)
因为T
既不是不可为空,也不是Diff<T, U>
。如果输入类型T
,它将始终返回相同的值。您必须指示编译器怎么做
您必须定义包装OptionValue<T>
的新类型:
type OptionValue2<T> = OptionValue<NotNullable<T>>;
let getValue = <T>(value: T): OptionValue2<T> => ({
option: value ? "some" : "none",
value
});
let someValue: undefined;
let value = getValue(someValue); // this type will be OptionValue<never>
// still use OptionValue
let handleSomeValue = <T>(obj: OptionValue<T>): SomeValueReturn<T> => {
switch (obj.option) {
case "some":
return obj.value as T;
default:
return "empty" as "empty";
}
};
let someUndefinedValue: string | null | undefined = undefined;
对于最后一种情况,Typescript编译器仅评估类型。它不知道结果是字符串还是null。您不能期望动态价值评估