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;
}
let val: unknow
won't work - you need ann
– Hollandsunknown
or how TS interpreters parentheses, not about a comparison toany
– Hollandsunknown
. EDIT: Hmm, no, it doesn't appear to be. Making itlet val: string | undefined
doesn't exhibit the same behaviour. EDIT2: Well, my bad. It actually is a type guard issue: tsplay.dev/WK7VKW – Falknertypeof
keyword and that TS is not able to recognize this as a typeguard anymore. Ambiguity:type T = typeof val;
vslet 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 writeif(t === "string") {...}
– Granado