这里有很多东西需要解包,但简短的版本是,您需要使用显式类型注释才能使其工作,推理有它的限制。
首先是
handleSomeValue
是
<T>(obj: OptionValue<T>) => T | "empty"
. 注意,两者之间没有关系
T
以及是否
'empty'
T | "empty"
. 所以为什么
'空'
T型
让我们考虑第一个例子
let someStringValue = 'check'; // type string
let result1 = handleSomeValue(getValue(someStringValue));
这里是
到
手的价值
将
string
,因此结果将是
string | 'empty'
"empty"
是
一串
所以字符串会吃掉文本类型
(因为它是多余的)结果会是
let someUndefinedValue: string | null | undefined = undefined;
let result3 = handleSomeValue(getValue(someUndefinedValue)); // it is 'empty' correctly;
在这里的时候
someUndefinedValue
string | null | undefined
事实上不是,如果你在上面盘旋
在第二行中,您将看到它被输入为
undefined
. 这是因为流分析确定实际类型将是
因为变量没有路径
未定义
getValue(someUndefinedValue)
会回来的
OptionValue<never>
T型
在里面
never
所以我们得到
never | 'empty'
. 从那以后
是所有类型的子类型(请参见
PR
)
从不|“空”
'空'
.
有趣的是当
实际上是
string | undefined
getValue
会回来的
'OptionValue<string> | OptionValue<never>'
编译器将无法推断
正确地。
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'
获取值
回报
OptionValue<number>
所以
将
number
结果就是
number | 'empty'
. 由于联合中的两个类型没有关系,编译器将不再尝试进一步简化联合,并将保持结果类型不变。
如您所料的解决方案,并保留
'空'
文字类型不可能,因为
字符串|“空”
永远都是
. 如果我们使用品牌类型来添加
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; }