Skip to content

Suggestion: Improve type reductions with absorption laws #16386

@jcalz

Description

@jcalz

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) and A & (A | B) should reduce to A.

  • Related are laws of subtype collapsing; that is, if B extends A, then A | B should reduce to A, and A & B should reduce to B. (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 or undefined|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}) when string 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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    SuggestionAn idea for TypeScript

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions