Reputation: 8226
I'm using Typescript 3.0.1. In the code below, why is there no compiler error on line 7? I used to have this behavior before; has it been taken out of Typescript or is there some weird regression?
type A = {type :"a"}
type B = {type :"b"}
type Any = A | B
function get<T extends Any>(x: T["type"]): T|undefined {
switch (x) {
case "x": return undefined
default: return undefined
}
}
Upvotes: 3
Views: 653
Reputation: 30929
The problem comes down to this code in checkSwitchStatement
in the checker, which has been there since 2016:
let caseType = checkExpression(clause.expression);
const caseIsLiteral = isLiteralType(caseType);
let comparedExpressionType = expressionType;
if (!caseIsLiteral || !expressionIsLiteral) {
caseType = caseIsLiteral ? getBaseTypeOfLiteralType(caseType) : caseType;
comparedExpressionType = getBaseTypeOfLiteralType(expressionType);
}
if (!isTypeEqualityComparableTo(comparedExpressionType, caseType)) {
// expressionType is not comparable to caseType, try the reversed check and report errors if it fails
checkTypeComparableTo(caseType, comparedExpressionType, clause.expression, /*headMessage*/ undefined);
}
(There is similar code that affects a direct comparison x === "x"
.)
The rules for a === b
and the analogous switch statement are based on the concept of bidirectional type "comparability", which (omitting a lot of detail that is irrelevant here) says that a union constituent of one side has to be assignable to a union constituent of the other side. This is supposed to be a heuristic for whether the types of the two sides overlap. The heuristic works well for the ways objects are typically used but not so well for primitives, where (for example) if the type of a
is some type parameter T
constrained by string
, we want to be able to compare it to "x"
; neither T
nor "x"
is known to be assignable to the other, but T
may include "x"
. So when one side of the comparison is a union of literals but the other is not, the code replaces the side that is a union of literals by the underlying primitive type. That case is triggering in your code, where "x"
is a literal and T["type"]
is not itself a literal, though it is constrained by a union of literals.
I think we should file an issue proposing that your code should give a compile error. And after I wrote that sentence, I saw artem filed an issue, so I will add my analysis there.
Re your belief that you got an error before, maybe you were thinking of the following code. The type of a property access on a type parameter is eagerly resolved if possible based on the constraint of the type parameter, so y.type
is considered as having type "a" | "b"
, and both sides of the comparison are a union of literal types, so the special case does not apply.
type A = {type :"a"}
type B = {type :"b"}
type Any = A | B
function get<T extends Any>(y: T): T | undefined {
switch (y.type) {
case "x": return undefined
default: return undefined
}
}
Upvotes: 1