Skip to content

Suggestion: Improve type reductions with absorption laws #16386

Closed
@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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions