【问题标题】:TypeScript inference doesn't work properlyTypeScript 推理无法正常工作
【发布时间】:2018-08-13 02:53:50
【问题描述】:

假设我们有以下定义,我不明白为什么 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链接

【问题讨论】:

  • someValue 的隐式类型为 any。难道你不希望getValue(someValue) 返回一个OptionValue&lt;any&gt;
  • 根据这些文档不确定它应该以这种方式工作:typescriptlang.org/docs/handbook/release-notes/…
  • getValue的返回类型为OptionValue&lt;T&gt;,在调用处,T被推断为参数someValue的类型,即string | undefined。因此实例化的返回类型是OptionValue&lt;string | undefined&gt;。为什么你期望它是OptionValue&lt;never&gt;
  • 你是对的。不能是OptionValue&lt;never&gt;,但即使这样也无助于正确获得结果。
  • @MattMcCutchen 我更新了问题以关注您的评论。请检查一下。谢谢。

标签: javascript typescript type-inference


【解决方案1】:

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

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

首先推断handleSomeValue的签名是&lt;T&gt;(obj: OptionValue&lt;T&gt;) =&gt; 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 上,您会看到它输入为undefined。这是因为流分析确定实际类型将为undefined,因为没有变量为undefined 的路径。

这意味着getValue(someUndefinedValue) 将返回OptionValue&lt;never&gt;,所以handleSomeValue 中的T 将是never,所以我们得到never | 'empty'。并且由于never 是所有类型的子类型(参见PR),never | 'empty' 将只计算为'empty'

有趣的是,当someUndefinedValue 实际上是string | undefined 时,示例无法编译,因为getValue 将返回'OptionValue&lt;string&gt; | OptionValue&lt;never&gt;',编译器将无法正确推断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&lt;number&gt;,因此handleSomeValue 中的T 将是number,因此结果将是number | 'empty'。由于 union 中的两种类型没有关系,编译器不会尝试进一步简化 union,将保持结果类型不变。

解决方案

无法按预期工作并保留'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; }

【讨论】:

  • 首先感谢您抽出宝贵时间回答问题。根据您的解释,TypeScript 编译器仅在两个完整的接口/结构没有任何共同模式应该能够正确推断类型时才会有所不同。
  • ...恕我直言,您的解决方案增加了我的示例的复杂性。我在界面中使用选项的全部原因是让编译器对具有或不具有值的结构有一个线索,并能够区分并因此推断。对我来说,您将 isEmpty 添加为布尔值与选项为“一些”或“无”相同。现在我的问题是,是否可以简化这个问题或者由于语言的限制而无法解决?
  • @AminPaks isEmpty 的原因是您希望使用字符串值来具有特殊含义。如果Tstring 那里有问题,编译器会正确地认为string | 'empty' 只是string 为什么不应该呢?如果我们使字符串'empty' 与其他字符串不同,我们可以让编译器知道它不应该这样,即我们以其他方式使其特殊。您可以改用专用的enum,这会让编译器知道,
  • 您会根据您的最新评论使用解决方案更新您的答案吗?谢谢
  • @AminPaks 我只是尝试使用枚举来实现,但枚举的扩展方式还有一些其他问题。 (由于某种原因没有保留枚举文字类型)如果你想玩它,这里是它的要点:gist.github.com/dragomirtitian/696bcba440342aaa8fc964d1fcca5075
【解决方案2】:

handleSomeValue 的推断返回类型被计算为所有返回表达式的类型的并集,即T | "empty"。在每个调用站点,此返回类型使用T 的类型参数实例化。 TypeScript 不会为每个调用重新分析 handleSomeValue 的主体,以查看根据 T 可以访问的 switch 的哪些情况。如果你愿意,你可以像这样注释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>;
  }
}

但是我真的不明白你想要达到什么目的。

【讨论】:

  • 类型推断的重点是不要给类型系统这么多细节,让它工作。到目前为止,我们重复了太多次以达到这一点。我试图在 TypeScript 中实现一个简单的 monad,但因为不值得而放弃了。
【解决方案3】:

因为T 既不是 NotNullable 也不是 Diff&lt;T, U&gt;。如果你输入类型T,它总是会返回相同的。你必须告诉编译器怎么做

你必须定义包装OptionValue&lt;T&gt;的新类型:

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。你不能指望动态的价值评估

【讨论】:

    猜你喜欢
    • 2020-05-07
    • 2018-06-27
    • 2016-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-14
    相关资源
    最近更新 更多