5

In typescript:

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

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

but, if I change the code a little bit:

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

TS Playground link

This is really confusing me, who can help to explain what happened here.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
Yue Dong
  • 85
  • 5
  • 1
    `let val: unknow` won't work - you need an `n` – CertainPerformance Nov 15 '21 at 23:24
  • Does this answer your question? ['unknown' vs. 'any'](https://stackoverflow.com/questions/51439843/unknown-vs-any) – jsejcksn Nov 15 '21 at 23:24
  • 2
    @jsejcksn Don't think it does - this is about a particular quirk of `unknown` or how TS interpreters parentheses, not about a comparison to `any` – CertainPerformance Nov 15 '21 at 23:25
  • @CertainPerformance might even be something to do with type guards, not even specific to `unknown`. EDIT: Hmm, no, it doesn't appear to be. Making it `let val: string | undefined` doesn't exhibit the same behaviour. EDIT2: Well, my bad. It actually is a type guard issue: https://tsplay.dev/WK7VKW – VLAZ Nov 15 '21 at 23:26
  • I think that this has to do with the ambiguity of the `typeof` keyword and that TS is not able to recognize this as a typeguard anymore. Ambiguity: `type T = typeof val;` vs `let t = typeof val;` I'd guess that the parentheses turn this into the latter so that your condition is downgraded from a typeguard to a comparison of two strings, no differrent than if you'd write `if(t === "string") {...}` – Thomas Nov 16 '21 at 00:10
  • see https://github.com/microsoft/TypeScript/issues/42203 – jcalz Nov 16 '21 at 00:23

1 Answers1

6

TypeScript's typeof type guards walk a fine line. typeof val is a string, and you can do arbitrary string operations to it, but typeof val === "string" is a special construction that narrows the type of val when the expression is true. Consequently, TypeScript is explicitly programmed to match typeof ${reference} ${op} ${literal} and ${literal} ${op} typeof ${reference} (for op = ==, !=, ===, and !==), but typeof ${reference} has no tolerance built in for parentheses (which is a SyntaxKind.ParenthesizedExpression and not a SyntaxKind.TypeOfExpression), string manipulation, or anything else.

TypeScript lead Ryan Cavanaugh describes this in microsoft/TypeScript#42203, "typeof type narrowing acts differently with equivalent parentheses grouping", with gratitude to jcalz for the link:

Narrowings only occur on predefined syntactic patterns, and this isn't one of them. I could see wanting to add parens here for clarity, though -- we should detect this one too.

It sounds like this is a candidate for a future fix, though even if the pattern were added, you would still be somewhat limited in the complexity of typeof expressions that work as type guards.


From compiler source microsoft/TypeScript main/src/compiler/checker.ts, comments mine:

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;
}
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251