为啥使用“if-else”语句会在看似相同的三元运算符构造不会产生 TypeScript 编译器错误?

Posted

技术标签:

【中文标题】为啥使用“if-else”语句会在看似相同的三元运算符构造不会产生 TypeScript 编译器错误?【英文标题】:Why does using an 'if-else' statement produce a TypeScript compiler error when a seemingly identical ternary operator construct does not?为什么使用“if-else”语句会在看似相同的三元运算符构造不会产生 TypeScript 编译器错误? 【发布时间】:2020-08-24 08:57:33 【问题描述】:

我有一个函数旨在返回值IDBValidKey 或转换为IDBValidKey 的值。如果我使用三元运算符编写函数,它可以正常工作,但如果我将其编写为 if-else 语句,则会导致编译器错误:

interface IDBValidKeyConvertible<TConverted extends IDBValidKey> 
    convertToIDBValidKey: () => TConverted;


function isIDBValidKeyConvertible<TConvertedDBValidKey extends IDBValidKey>(object: unknown): object is IDBValidKeyConvertible<TConvertedDBValidKey> 
    return typeof((object as IDBValidKeyConvertible<TConvertedDBValidKey>).convertToIDBValidKey) === "function";


type IDBValidKeyOrConverted<TKey> = TKey extends IDBValidKeyConvertible<infer TConvertedKey> ? TConvertedKey : TKey;

function getKeyOrConvertedKey<TKey extends IDBValidKey | IDBValidKeyConvertible<any>>(input: TKey): IDBValidKeyOrConverted<TKey> 
    if (isIDBValidKeyConvertible<IDBValidKeyOrConverted<TKey>>(input)) 
        return input.convertToIDBValidKey();
     else 
        return input;
    


function getKeyOrConvertedKeyTernary<TKey extends IDBValidKey | IDBValidKeyConvertible<any>>(input: TKey): IDBValidKeyOrConverted<TKey> 
    return (isIDBValidKeyConvertible<IDBValidKeyOrConverted<TKey>>(input)) ? input.convertToIDBValidKey() : input;

getKeyOrConvertedKeyTernary 不会产生错误,但 getKeyOrConvertedKeyelse 块会产生此错误:

Type 'TKey' is not assignable to type 'IDBValidKeyOrConverted<TKey>'.
  Type 'string | number | Date | ArrayBufferView | ArrayBuffer | IDBArrayKey | IDBValidKeyConvertible<any>' is not assignable to type 'IDBValidKeyOrConverted<TKey>'.
    Type 'string' is not assignable to type 'IDBValidKeyOrConverted<TKey>'.

三元运算符和 if-else 语句不是等价的吗?

谢谢!

【问题讨论】:

查看 AST 似乎区别在于 ReturnStatement &gt; ConditionalExpression &gt; Binary Expression &gt; ...IfStatement &gt; ((BinaryExpression &gt; ...) + (Block ReturnStatement) + (Block ReturnStatement)。话虽如此,我不确定为什么会导致这个结果......好问题! 我的直觉是应用类型保护的范围与三元组并不真正兼容。除非 Ryan Cavanaugh 或 Anders Hejlsberg 在发布问题之前阅读此问题,否则很可能是 Github 上某个问题的好人选。 【参考方案1】:

存在两个完全不同的问题

    Typescript 存在错误或限制[1],但它与您的问题所假设的相反

    如果我使用三元运算符编写函数,它可以正常工作

    其实,这样不好。 if-else 版本的错误是正确的,三元版本的错误应该是一样的。

    您可能做出了这样的假设,因为和我们大多数人一样,我们倾向于假设我们的代码是正确的。这就引出了第二个问题:


    您的代码不合逻辑。 (请见谅。)

我认为(使用问题中现在简化的代码)#2 应该更容易看到。不过等我有空的时候我会尽量详细解释的。


[1] 这可能不是 Typescript 缺陷,而是由于我不知道的 if-else? : 之间的一些微妙的不等价而导致的预期行为。我不会感到惊讶,因为 javascript 有很多遗留的怪异之处。 [编辑:见Shaun Luttin's answer,正如我正在输入我的一样。]

【讨论】:

【参考方案2】:

为什么使用“if-else”语句会产生 TypeScript 编译器错误,而看似相同的三元运算符构造却没有?

简答

TypeScript 将 if-else 视为包含多个表达式的语句,每个表达式都有独立的类型。 TypeScript 将三元视为一个表达式,它具有真假两面的联合类型。有时,联合类型变得足够宽,编译器不会抱怨。

详细解答

三元运算符和 if-else 语句不是等价的吗?

不完全是。

区别在于三元是一个表达式。有一个conversation here,Ryan Cavanaugh 解释了三元和 if/else 语句之间的区别。值得一提的是,三元表达式的类型是其truefalse 结果的联合。

对于您的特定情况,您的三元表达式的类型是any。这就是编译器不抱怨的原因。您的三元是 input 类型和 input.convert() 返回类型的联合。在编译时,input 类型扩展了Container&lt;any&gt;;因此input.convert() 返回类型为any。因为any 的联合是any,所以你的三元类型是any

您的快速解决方案是将&lt;TKey extends IDBValidKey | IDBValidKeyConvertible&lt;any&gt; 中的any 更改为unknown。这将使 if-else 和三元都产生编译器错误。

简化示例

这里是a playground link,其中简化了您的问题。尝试将any 更改为unknown 以查看编译器如何响应。

interface Container<TValue> 
  value: TValue;


declare function hasValue<TResult>(
  object: unknown
): object is Container<TResult>;

// Change any to unknown.
const funcIfElse = <T extends Container<any>>(input: T): string => 
  if (hasValue<string>(input)) 
    return input.value;
  

  return input;
;

// Change any to unknown.
const funcTernary = <T extends Container<any>>(input: T): string =>
  hasValue<string>(input)
    ? input.value
    : input;

【讨论】:

很高兴找到肖恩!我链接到我的答案。我不太明白 Ryan Cavanaugh 关于为什么 Typescript 不能做得更好的解释,但我的头很累:P 感谢您抽出宝贵时间阅读问题并提供解释;它帮助我理解了这个问题!

以上是关于为啥使用“if-else”语句会在看似相同的三元运算符构造不会产生 TypeScript 编译器错误?的主要内容,如果未能解决你的问题,请参考以下文章

三元?运算符与 C# 中的传统 If-else 运算符 [重复]

三元运算符的速度是 if-else 块的两倍?

为啥以及何时使用重组分支?

Python:为啥 if-else 一行语句在 else 中不能与 continue 一起使用?

为啥 Chrome 会在我的 HTML 顶部添加一个正文,然后给我一个看似虚假的错误消息?

为啥在 C++ 中使用 if-else if?