javascript – 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;

Playground链接

最佳答案 这里有很多解包方法,但简短的版本是你需要使用显式类型注释才能使它工作,推理有它的限制.

有趣的是,看看为什么这显然可以像你期望的那样在某些情况下起作用.

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

让我们考虑第一个例子

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

这里T to handleSomeValue将是字符串,因此结果将是字符串| ’empty’但是“empty”是字符串的子类型,因此字符串会占用文字类型“empty”(因为它是多余的),结果将是字符串

现在让我们看看第三个也可以工作的例子:

let someUndefinedValue: string | null | undefined = undefined;
let result3 = handleSomeValue(getValue(someUndefinedValue)); // it is 'empty' correctly;

虽然这里有一些UndefinedValue似乎被输入为字符串| null | undefined它实际上不是,如果你将鼠标悬停在第二行的someUndefinedValue上,你会看到它被输入为undefined.这是因为流分析确定实际类型将是未定义的,因为没有未定义变量的路径.

这意味着getValue(someUndefinedValue)将返回OptionValue< never>,因此handleSomeValue中的T将永远不会这样我们永远不会“空”.并且因为永远不是所有类型的子类型(见PR)从不| ’empty’将评估为’empty’.

有趣的是,当someUndefinedValue实际上是字符串|时undefined示例无法编译,因为getValue将返回’OptionValue< string> | OptionValue<从未>”并且编译器将无法正确推断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>所以handleSomeValue中的T将是数字,因此结果将是数字| “空”.由于union中的两个类型没有关系,编译器不会尝试进一步简化并集,并将结果类型保留为原样.

解决方案

一个按预期工作并保留’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; }
点赞