javascript - TypeScript 推理无法正常工作
问题描述
假设我们有以下定义,我不明白为什么 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;
游乐场链接
解决方案
这里有很多东西要解压,但简短的版本是您需要使用显式类型注释才能使其工作,推理有其局限性。
有趣的是,为什么这在某些情况下显然可以按您的预期工作。
首先推断的签名handleSomeValue
是<T>(obj: OptionValue<T>) => T | "empty"
。T
请注意,与是否包含在返回类型中之间没有关系'empty'
,结果始终为T | "empty"
. 那么为什么'empty'
有时会丢失,T
有时会丢失。嗯,这与如何评估工会的规则有关。
让我们考虑第一个例子
let someStringValue = 'check'; // type string
let result1 = handleSomeValue(getValue(someStringValue));
这里T
tohandleSomeValue
将是string
,所以结果将是string | 'empty'
but"empty"
的子类型,string
所以 string 会吃掉文字类型"empty"
(因为它是多余的),结果将是string
现在让我们看一下似乎也有效的第三个示例:
let someUndefinedValue: string | null | undefined = undefined;
let result3 = handleSomeValue(getValue(someUndefinedValue)); // it is 'empty' correctly;
虽然这里someUndefinedValue
似乎是输入的,string | null | undefined
但实际上并非如此,但如果您将鼠标悬停someUndefinedValue
在第二行,您会看到它输入为undefined
. 这是因为流分析确定实际类型将是undefined
,因为变量没有路径undefined
。
这意味着getValue(someUndefinedValue)
将返回OptionValue<never>
,所以T
在handleSomeValue
将是never
我们得到never | 'empty'
。因为never
is 所有类型的子类型(参见PR)never | 'empty'
只会评估为'empty'
.
有趣的someUndefinedValue
是,示例实际上何时string | undefined
无法编译,因为getValue
将返回'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
inhandleSomeValue
将是number
,结果将是number | 'empty'
. 由于联合中的两种类型没有关系,因此编译器不会尝试进一步简化联合,而是将结果类型保持原样。
解决方案
无法按您期望的方式工作并且保留'empty'
文字类型的解决方案是不可能的,因为 的并集string | 'empty'
将始终为string
. 如果我们使用品牌类型添加一些东西来防止简化,我们可以防止empty
简化。此外,我们将需要一个显式类型注释来正确识别返回类型:
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 }) as any;
type GetOptionValue<T extends OptionValue<any> | OptionValue<never>> =
T extends OptionValue<never> ? ('empty' & { isEmpty: true }) :
T extends OptionValue<infer U> ? U: never ;
let handleSomeValue = <T extends OptionValue<any> | OptionValue<never>>(obj: T) : GetOptionValue<T>=> {
switch (obj.option) {
case 'some':
return obj.value;
default:
return 'empty' as GetOptionValue<T>;
}
}
let someStringValue = 'check'; // type string
let result1 = handleSomeValue(getValue(someStringValue)); // it is 'string' correctly
let someNumberValue = 22;
let result2 = handleSomeValue(getValue(someNumberValue)); //is number
let someStringOrUndefinedValue: string | null | undefined = Math.random() > 0.5 ? "" : undefined;
let result3 = handleSomeValue(getValue(someStringOrUndefinedValue)); // is string | ("empty" & {isEmpty: true;})
let someUndefinedValue: undefined = undefined;
let result4 = handleSomeValue(getValue(someUndefinedValue)); // is "empty" & { isEmpty: true; }