为啥括号会影响 TypeScript 中的类型缩小?

Posted

技术标签:

【中文标题】为啥括号会影响 TypeScript 中的类型缩小?【英文标题】:Why do parentheses affect type narrowing in TypeScript?为什么括号会影响 TypeScript 中的类型缩小? 【发布时间】:2021-12-27 02:44:06 【问题描述】:

在打字稿中:

let str: string = 'abc';
let val: unknown = 'test';

if (typeof val === 'string') 
    str = val;
 
// this code does not report any error, everything works fine.

但是,如果我稍微更改一下代码:

if ((typeof val) === 'string')  
    str = val; 
 
// add the () to hold typeof val;
// error report in typescript in this line: str = val !

TS Playground link

这真的让我很困惑,谁能帮忙解释一下这里发生了什么。

【问题讨论】:

let val: unknow 不起作用 - 你需要一个 n 这能回答你的问题吗? 'unknown' vs. 'any' @jsejcksn 不要认为是这样 - 这是关于 unknown 的特殊怪癖或 TS 解释器如何使用括号,而不是与 any 进行比较 @CertainPerformance 甚至可能与类型保护有关,甚至与unknown 无关。编辑:嗯,不,它似乎不是。使其成为let val: string | undefined 不会表现出相同的行为。 EDIT2:好吧,我的错。这实际上是一个类型保护问题:tsplay.dev/WK7VKW 我认为这与 typeof 关键字的歧义有关,并且 TS 不再能够将其识别为类型保护。歧义:type T = typeof val; vs let t = typeof val; 我猜括号把它变成了后者,这样你的条件就从类型保护降级为两个字符串的比较,与你写 if(t === "string") ... 没有什么不同跨度> 【参考方案1】:

TypeScript 的 typeof type guards 走得很好。 typeof val 是一个字符串,你可以对它进行任意字符串操作,但是typeof val === "string" 是一个特殊的构造,当表达式为true 时,它会缩小val 的类型。因此,TypeScript 被明确编程为匹配 typeof $reference $op $literal$literal $op typeof $reference(对于 op = ==!====!==),但 typeof $reference 没有内置的括号容差(是 SyntaxKind.ParenthesizedExpression 而不是 SyntaxKind.TypeOfExpression)、字符串操作或其他任何东西。

TypeScript 负责人 Ryan Cavanaugh 在 microsoft/TypeScript#42203 中描述了这一点,“typeof 类型缩小的行为与等效括号分组不同”,感谢 jcalz 提供的链接:

缩窄只发生在预定义的句法模式上,这不是其中之一。不过,为了清楚起见,我可以看到想要在此处添加括号 - 我们也应该检测到这个。

这听起来像是未来修复的候选者,尽管即使添加了该模式,您仍然会在一定程度上受限于用作类型保护的 typeof 表达式的复杂性。


来自编译器源microsoft/TypeScript main/src/compiler/checker.ts,我的cmets:

function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type 
  switch (expr.operatorToken.kind) 
    // ...
    case SyntaxKind.EqualsEqualsToken:
    case SyntaxKind.ExclamationEqualsToken:
    case SyntaxKind.EqualsEqualsEqualsToken:
    case SyntaxKind.ExclamationEqualsEqualsToken:
        const operator = expr.operatorToken.kind;
        const left = getReferenceCandidate(expr.left);
        const right = getReferenceCandidate(expr.right);
        // Check that the left is typeof and the right is a string literal...
        if (left.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(right)) 
            return narrowTypeByTypeof(type, left as TypeOfExpression, operator, right, assumeTrue);
        
        // ...or the opposite...
        if (right.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(left)) 
            return narrowTypeByTypeof(type, right as TypeOfExpression, operator, left, assumeTrue);
        
        // ...or skip it and move on. Don't bother trying to remove parentheses
        // or doing anything else clever to try to make arbitrary expressions work.
        if (isMatchingReference(reference, left)) 
            return narrowTypeByEquality(type, operator, right, assumeTrue);
        
        if (isMatchingReference(reference, right)) 
            return narrowTypeByEquality(type, operator, left, assumeTrue);
        
        // ...
  
  return type;

【讨论】:

你也可以参考ms/TS#42203 @jcalz 不错的发现!我已经在正文中添加了它和相关的引用。谢谢!

以上是关于为啥括号会影响 TypeScript 中的类型缩小?的主要内容,如果未能解决你的问题,请参考以下文章

在 ko 绑定中包含括号会影响评估顺序吗?为啥?

是否可以将 TypeScript 编译成缩小代码?

为啥初始化列表允许 C++ 中的类型缩小?

为啥 TypeScript 中的类允许使用鸭子类型

TypeScript 类型推断/缩小挑战

如何使用泛型缩小 TypeScript 联合类型