首页 > 解决方案 > 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;

游乐场链接

标签: javascripttypescripttype-inference

解决方案


这里有很多东西要解压,但简短的版本是您需要使用显式类型注释才能使其工作,推理有其局限性。

有趣的是,为什么这在某些情况下显然可以按您的预期工作。

首先推断的签名handleSomeValue<T>(obj: OptionValue<T>) => T | "empty"T请注意,与是否包含在返回类型中之间没有关系'empty',结果始终为T | "empty". 那么为什么'empty'有时会丢失,T有时会丢失。嗯,这与如何评估工会的规则有关。

让我们考虑第一个例子

let someStringValue = 'check'; // type string
let result1 = handleSomeValue(getValue(someStringValue));

这里TtohandleSomeValue将是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>,所以ThandleSomeValue将是never我们得到never | 'empty'。因为neveris 所有类型的子类型(参见PRnever | '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>所以TinhandleSomeValue将是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; }

推荐阅读