Description
The type checker currently reduces/compares unions and intersections using the laws of idempotence (e.g., A & A
= A
), associativity (e.g., (A | B) | C
= A | (B | C)
), commutativity (e.g., A & B
= B & A
), and distributivity (e.g., A | (B & C)
= (A | B) & (A | C)
).
-
I think it would be great to introduce the absorption laws: That is,
A | (A & B)
andA & (A | B)
should reduce toA
. -
Related are laws of subtype collapsing; that is, if
B extends A
, thenA | B
should reduce toA
, andA & B
should reduce toB
. (Issue Spec Preview: Union types #805 implies that this already happens for unions, but I am not seeing the type checker doing an actual reduction.) -
Finally, maybe slightly off-topic, it would be nice to reduce intersections of known-disjoint types to
never
orundefined|null
depending on the strictness of null checks.
I should note that currently the type checker does consider all such equivalent types mutually compatible (meaning that a value of type A | (A & B)
can be assigned to a variable of type A
and vice-versa), even though they don't reduce to the same type. This is good, but actual reductions for absorption laws would:
- improve completeness (right now there are expressions incorrectly flagged as type errors)
- increase transparency/legibility (neither the type checker nor developers should want to drag around such unwieldy beasts as
string & (string | number) & (string | object) & (string | {ad: number, nauseum: string})
whenstring
would be better for everyone involved).
I don't know how expensive it would be to perform such reduction rules in the compiler. Before I or anyone looks into that, though, I'd be interested in finding out if such a feature would even be considered useful. Thoughts?
Code examples! Everywhere below that produces the error // Error, subsequent variable declarations must have the same type
should type-check:
var o = {};
// Absorption Law
function absorption<A, B>() {
var x: A & (A | B);
var x: A | (A & B);
var x: A; // Error, subsequent variable declarations must have the same type.
// but these assignments work
var y: A = o as A | (A & B);
var z: A | (A & B) = o as A;
}
// Subtype Collapsing
function absorptionExtends<A, B extends A>() {
var x: A & B;
var x: B; // Error, subsequent variable declarations must have the same type.
// but these assignments work
var y: B = o as A & B;
var z: A & B = o as B;
var u: A | B;
var u: A; // Error, subsequent variable declarations must have the same type.
// but these assignments work
var v: A = o as A | B;
var w: A | B = o as A;
}
// Intersections of known-disjoint types:
var x: string & number
x = '0'; // Error as expected
x = 0; // Error as expected
x = undefined; // Error as expected with strictNullChecks
x = null; // Error as expected with strictNullChecks
// but
var x: undefined | null; // Error, subsequent variable declarations must have the same type.
var x: never; // Error, subsequent variable declarations must have the same type.
// EDIT: maybe plays badly with tagged types?
var y: string & object;
y = 'hmm'; // Error as expected
y = {}; // Error as expected
y = undefined; // Error as expected with strictNullChecks
y = null; // Error as expected with strictNullChecks
// but
var y: undefined | null; // Error, subsequent variable declarations must have the same type.
var y: never; // Error, subsequent variable declarations must have the same type.
/*---*/
// if absorptionExtends is too expensive in general,
// at least some specifics for string/number literals would be nice:
absorptionExtends<string, 'a' | 'b'>();
absorptionExtends<number, 0 | 1>();
// bottoms should absorb all intersections and be absorbed by all unions
// but see https://github.com/Microsoft/TypeScript/issues/16038
function neverAbsorption<T>() {
absorptionExtends<T, never>();
}
// tops should absorb all unions and be absorbed by all intersections
function emptyObjectAbsorption<T>(){
absorptionExtends<{}, T>();
}
// I guess "any" is a special case
// as mentioned in https://github.com/Microsoft/TypeScript/issues/10715
function anyAbsorption<T>() {
absorptionExtends<any, T>();
absorptionExtends<T, any>();
}
// here we build up a complex type with a simple reduction
var i: string;
var j: typeof i | number;
var k: typeof i | object;
var l: typeof i | { ad: number, nauseum: string };
var m: typeof i & typeof j & typeof k & typeof l;
// let's look at typeof m:
var m: string | (string & { ad: number; nauseum: string; }) | (string & object) |
(string & object & { ad: number; nauseum: string; }) | (string & number) |
(string & number & { ad: number; nauseum: string; }) | (string & number & object) |
(string & number & object & { ad: number; nauseum: string; });
// yuck! typeof m should be string
var m: string; // Error, subsequent variable declarations must have the same type.
// but these assignments work
var n: string;
n = m;
n = null;
m = n;
Cheers!