代码之家  ›  专栏  ›  技术社区  ›  Amin Paks

类型脚本推理无法正常工作

  •  4
  • Amin Paks  · 技术社区  · 6 年前

    假设我们有以下定义,我不明白为什么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 回复  |  直到 6 年前
        1
  •  1
  •   Titian Cernicova-Dragomir    6 年前

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

    首先是 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; }
    
        2
  •  0
  •   Matt McCutchen    6 年前

    推断的返回类型 handleSomeValue 计算为所有返回表达式类型的并集,即。, T | "empty" . 在每个调用站点,此返回类型都用 T 手的价值 T型 这样地:

    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>;
      }
    }
    

        3
  •  0
  •   hgiasac    6 年前

    因为 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编译器只计算类型。它不知道结果是string还是null。你不能期望动态价值评估