UPDATED QUESTION (single primary question)
Given the code snippet below, why is the type narrowed when declaring a const to be Version
but not for one which is declared as number
?
type Version = 1 | 2 | 3;
const v: Version = 3;
if (v === 2) console.log('v equals 2'); // Fails compilation with "This comparison appears to be unintentional
// because the types '3' and '2' have no overlap."
const n: number = 3;
if (n === 2) console.log('n equals 2'); // No compile error
ORIGINAL POST
Given the code snippet below:
...types '3' and '2'...
rather than ...types 'Version' and '2'
?type Version = 1 | 2 | 3;
const v: Version = 3;
if (v > 2) console.log('v is greater than 2');
if (v === 3) console.log('v equals 3');
if (v === 2) console.log('v equals 2'); // Fails compilation with "This comparison appears to be unintentional
// because the types '3' and '2' have no overlap."
See microsoft/TypeScript#16976 for an authoritative answer to this question.
Generally speaking, narrowing tends only to operate by filtering union types. In
type Version = 1 | 2 | 3;
const v: Version = 3;
((v));
//^? const v: 3
TypeScript performs assignment narrowing on v
from the union type 1 | 2 | 3
to just those members assignable to the initializer. That's 3
, so v
is seen as having the type 3
from then on.
On the other hand,
const n: number = 3;
((n));
//^? const n: number
no useful narrowing is performed. The type number
is not a union. If you had something like
const sn: string | number = 3;
((sn));
//^? const sn: number
then you'd see narrowing from string | number
to number
, because 3
is assignable to number
but not string
.
So why doesn't narrowing happening for non-union types? Well, the obvious and unhelpful answer is that it hasn't been implemented. According to this comment in microsoft/TypeScript#16976,
Currently there's no logic in effect for "narrowing" of non-unions.
But the actual reason seems to be that it would be more complicated and difficult to do. For example in there I've linked to a relevant comment in microsoft/TypeScript#8513:
The question of narrowing non-union types on assignment is one that we're still thinking about. It gets somewhat more complicated because [...] when optional properties are involved, the assigned type may actually have fewer members than the declared type. One possible mitigation is to use an intersection of the declared type and the assigned type:
let a: { x?: number, y?: number }; a = { x: 1 }; a; // Type { x: number, y: number | undefined };
And I've also linked to a relevant issue, microsoft/TypeScript#10065 where it's said that
Continuing to think about this but it'd be very expensive -- we'd be resynthesizing a type [...] after every assignment.
It seems that filtering unions never introduces new types that TypeScript has to track (because the union already contains all the relevant types), but narrowing non-unions will often require TypeScript to synthesize and keep track of new types (like new intersections of the declared type and the assigned type), and this is expensive because it means extra work for every assignment.