TypeScript推断无法正常工作

时间:2018-08-13 02:53:51

标签: javascript typescript type-inference

让我们想象一下,我们有以下定义,我不明白为什么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;

Playground链接

3 个答案:

答案 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));

此处ThandleSomeValue将是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。您不能期望动态价值评估