为啥 && 运算符会产生第二个操作数的类型

Posted

技术标签:

【中文标题】为啥 && 运算符会产生第二个操作数的类型【英文标题】:Why does the && operator produce the type of the second operand为什么 && 运算符会产生第二个操作数的类型 【发布时间】:2012-09-23 12:18:42 【问题描述】:

TypeScript 规范在 §4.15.6 中说明了 && 运算符:

&& 运算符允许操作数为任何类型,并产生与第二个操作数相同类型的结果

javascript 中,&& 运算符如果为假则返回第一个操作数,否则返回第二个操作数 (see ECMA-262 §11.11)。

这意味着如果左操作数是假的,&& 将返回一个与左操作数类型匹配的值。例如,

typeof ( false &&       ) === "boolean" // true
typeof ( ''    && 1       ) === "string"  // true
typeof ( null  && "hello" ) === "object"  // true
typeof ( NaN   && true    ) === "number"  // true

根据上面引用的规则,Typescript 会错误地预测上述表达式的类型分别为ObjectNumberStringBoolean。 p>

我错过了什么吗?是否有充分的理由使 && 表达式的类型与第二个操作数的类型相匹配?结果类型不应该像||操作符一样,返回两个操作数中最好的通用类型,如果没有最好的通用类型则返回Any

【问题讨论】:

||的类型是什么? a ? b : c 呢?如果我理解正确,a && b 等同于a ? a : b(除了 a 在后一个示例中被评估两次) @ReyCharles && 就像 !a ? a : b|| 就像 a ? a : b 对,对不起。那是脑残。我真正的问题是a || ba ? b : c 的类型是什么。有趣的问题,顺便说一句。 @ReyCharles TypeScript 说a || b 的类型是Any,除非两个操作数有匹配的类型。在a ? b : c 中,如果我没记错的话,TypeScript 要求bc 是同一类型。 更新:如果 strictNullChecks 被启用,这现在可以在 Typescript 2 中正常工作。 【参考方案1】:

长话短说,这里没有让所有人都满意的解决方案。

考虑这个常见的成语:

var customer = GetCustomer(...); // of type 'Customer'
var address = customer && customer.address;
if(address) 
    printAddressLabel(address); // Signature: (Address) => void
 else 
    // Couldn't find the customer or the customer has no address on file

放弃并决定 'address' 是 'any' 是很糟糕的,因为在 Customer 和 Address 之间没有最好的通用类型。

在使用 && 运算符的大多数情况下,类型 已经 匹配,或者 && 以像上面那样的值合并方式使用。无论哪种情况,返回正确操作数的类型都会为用户提供预期的类型。

虽然此时类型安全在技术上已经崩溃,但它并没有以可能导致错误的方式这样做。要么您要测试结果值的真实性(在这种情况下,类型或多或少无关紧要),要么您将使用推定的右操作数进行某些操作(上面的示例两者都做)。

如果我们查看您列出的示例并假装左操作数不确定是真还是假,然后尝试编写可对返回值进行操作的合理代码,它就会变得更加清晰 - 您可以做的事情并不多 'false && ' 尚未进入“任何”参数位置或真实性测试。


附录

由于有些人不相信上述内容,这里有一个不同的解释。

让我们暂时假设 TypeScript 类型系统添加了三个新类型:Truthy<T>Falsy<T>Maybe<T>,表示 T 类型可能的真值/假值。这些类型的规则如下:

    Truthy<T> 的行为与 T 完全相同 您无法访问Falsy<T> 的任何属性 Maybe<T> 类型的表达式,当用作if 块中的条件时,在同一if 块的主体中​​变为Truthy<T>,在else 块中变为Falsy<T>

这会让你做这样的事情:

function fn(x: Maybe<Customer>) 
   if(x) 
      console.log(x.address); // OK
    else 
      console.log(x.phone); // Error: x is definitely falsy
   
   console.log(x.name); // Warning: x might be falsy!

到目前为止还不错。现在我们可以弄清楚 && 运算符的类型规则是什么。

Truthy&lt;T&gt; &amp;&amp; x 应该是一个错误 - 如果知道左边是真的,你应该写 x Falsy&lt;T&gt; &amp;&amp; x 应该是一个错误 - 如果知道左侧是假的,x 是无法访问的代码 Maybe&lt;T&gt; &amp;&amp; x 应该产生...什么?

我们知道Maybe&lt;T&gt; &amp;&amp; x 的结果要么是T 类型的假值,要么是x。它不能产生Truthy&lt;T&gt;(除非T == x 的类型,在这种情况下,整个讨论都没有实际意义)。我们称这种新类型为Falsy&lt;T&gt; XOR Maybe&lt;U&gt;

Falsy&lt;T&gt; XOR Maybe&lt;U&gt;的规则应该是什么?

显然,您不能在其上使用T 的属性。如果值是 T 类型,则它是虚假的,并且不安全。 您应该可以将其用作Maybe&lt;U&gt;,因为Falsy&lt;T&gt;Falsy&lt;U&gt; 具有相同的行为 您不应该使用U 的属性,因为该值仍然可能是虚假的。 如果您在 if 测试中使用它,那么它应该成为该 if 语句块中的 Truthy&lt;U&gt;

换句话说,Falsy&lt;T&gt; XOR Maybe&lt;U&gt; Maybe&lt;U&gt;。它遵循所有相同的规则。您根本不需要通过添加这个奇怪的XOR 类型来使类型系统复杂化,因为已经存在适合您需要的所有规范的类型。

这有点像给某人一个盒子,然后说“这要么是一箱空垃圾箱,要么是一整箱可回收物”。您可以安全地将盒子中的物品清空到回收箱中。

【讨论】:

但这真的不是它的工作方式......如果你写alert(typeof(false &amp;&amp; 'hello')),你会得到'boolean',而不是'string'。事实上,该打字稿代码编译成相同的 javascript。您是否声称 && 运算符的行为与 javascript 不完全相同? && 做什么 && 做什么,TypeScript 与否。问题是当参数的类型不同时,结果的 compile-time 类型应该是什么。在 "false && 'hello'" 的情况下,合理的选项是 bool、string 和 any;上面的解释是为什么选择'string'而不是'any'或'bool'的类型。 谢谢,这是有道理的。它仍然让我对编译时类型准确性感到不安,但我想这里的实用价值胜过技术正确性。 @chuckj & Ryan Cavanaugh,你是说为了编译的目的,它假定右手操作数的类型......明白了。这让我非常不舒服......正如彼得所说,它扰乱了我对编译时准确性的感受。就是感觉不对。 (尽管老实说,我觉得强类型的 javascript 是错误的。) 抱歉,这绝对是疯了。类型系统应该提供保证,而不是一些模糊准则的盗版代码。一个正确的答案(即,一个不会抛弃类型系统的全部观点的答案)是拥有两组逻辑运算符。一个,&& 和 ||,用于 JavaScript 的“我们不需要 steenkin 类型”的世界,结果类型为 any,另一个,比如 &&& 和 |||,用于 TypeScript,它们具有健全的语义和有用的类型规则. TypeScript 故意破坏了如此基本的东西,这让我感到非常震惊。【参考方案2】:

没有维护语言规范,在这种情况下是错误的。 &amp;&amp; 运算符的结果类型是右侧的类型和左侧的假值的并集。

来自TypeScript 2.0 release notes:

// Compiled with --strictNullChecks
declare function f(x: number): string;
let x: number | null | undefined;
// ..
let b = x && f(x); // Type of b is string | 0 | null | undefined

【讨论】:

以上是关于为啥 && 运算符会产生第二个操作数的类型的主要内容,如果未能解决你的问题,请参考以下文章

逗号运算符返回参数列表中的第一个值而不是第二个值?

Java逻辑运算符

泛谈JS逻辑判断选择器 || &&

为啥两个字节上的 xor 运算符会产生一个 int?

||和|,&&和&的区别

R语言逻辑操作符:&|!&&||