¿Por qué los paréntesis afectan la reducción de tipos en TypeScript?
En 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.
pero, si cambio el código un poco:
if ((typeof val) === 'string') {
str = val;
}
// add the () to hold typeof val;
// error report in typescript in this line: str = val !
Enlace de TS Playground
Esto realmente me está confundiendo, ¿quién puede ayudar a explicar qué ocurrió aquí?
Respuesta #1
Las guardas de tipo typeof de TypeScript caminan por una línea muy delgada. typeof val es una cadena, y puedes hacer operaciones arbitrarias con cadenas sobre ella, pero typeof val === "string" es una construcción especial que estrecha el tipo de val cuando la expresión es true. En consecuencia, TypeScript está explícitamente programado para coincidir con typeof $ {reference} $ {op} $ {literal} y $ {literal} $ {op} typeof $ {reference} (para op===, !=, === y !==), pero typeof $ {reference} no tiene tolerancia incorporada para paréntesis (que es un SyntaxKind.ParenthesizedExpression y no un SyntaxKind.TypeOfExpression), manipulación de cadenas, ni nada más.
El líder de TypeScript, Ryan Cavanaugh, describe esto en microsoft/TypeScript#42203, "el estrechamiento del tipo typeof actúa de manera diferente con un agrupamiento de paréntesis equivalente", con agradecimiento a jcalz por el enlace:
Las restricciones solo ocurren en patrones sintácticos predefinidos, y este no es uno de ellos. Sin embargo, podría entenderse querer agregar paréntesis aquí para mayor claridad; también deberíamos detectar este.
Parece que esto es un candidato para una corrección futura, aunque incluso si se añadiera el patrón, todavía estarías algo limitado en la complejidad de las expresiones typeof que funcionan como guardianes de tipo.
Desde el código fuente del compilador microsoft/TypeScript main/src/compiler/checker.ts, comentarios míos:
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;
}
