Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normalize union/intersection type combinations #11717

Merged
merged 4 commits into from
Oct 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 39 additions & 31 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5603,6 +5603,11 @@ namespace ts {
}
}

// We normalize combinations of intersection and union types based on the distributive property of the '&'
// operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection
// types with union type constituents into equivalent union types with intersection type constituents and
// effectively ensure that union types are always at the top level in type representations.
//
// We do not perform structural deduplication on intersection types. Intersection types are created only by the &
// type operator and we can't reduce those because we want to support recursive intersection types. For example,
// a type alias of the form "type List<T> = T & { next: List<T> }" cannot be reduced during its declaration.
Expand All @@ -5612,6 +5617,15 @@ namespace ts {
if (types.length === 0) {
return emptyObjectType;
}
for (let i = 0; i < types.length; i++) {
const type = types[i];
if (type.flags & TypeFlags.Union) {
// We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of
// the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain.
return getUnionType(map((<UnionType>type).types, t => getIntersectionType(replaceElement(types, i, t))),
/*subtypeReduction*/ false, aliasSymbol, aliasTypeArguments);
}
}
const typeSet = [] as TypeSet;
addTypesToIntersection(typeSet, types);
if (typeSet.containsAny) {
Expand Down Expand Up @@ -6559,52 +6573,46 @@ namespace ts {

const saveErrorInfo = errorInfo;

// Note that these checks are specifically ordered to produce correct results.
// Note that these checks are specifically ordered to produce correct results. In particular,
// we need to deconstruct unions before intersections (because unions are always at the top),
// and we need to handle "each" relations before "some" relations for the same kind of type.
if (source.flags & TypeFlags.Union) {
if (relation === comparableRelation) {
result = someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive));
}
else {
result = eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive));
}

if (result) {
return result;
}
}
else if (target.flags & TypeFlags.Intersection) {
result = typeRelatedToEachType(source, target as IntersectionType, reportErrors);

if (result) {
else if (target.flags & TypeFlags.Union) {
if (result = typeRelatedToSomeType(source, <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive))) {
return result;
}
}
else {
// It is necessary to try these "some" checks on both sides because there may be nested "each" checks
// on either side that need to be prioritized. For example, A | B = (A | B) & (C | D) or
// A & B = (A & B) | (C & D).
if (source.flags & TypeFlags.Intersection) {
// Check to see if any constituents of the intersection are immediately related to the target.
//
// Don't report errors though. Checking whether a constituent is related to the source is not actually
// useful and leads to some confusing error messages. Instead it is better to let the below checks
// take care of this, or to not elaborate at all. For instance,
//
// - For an object type (such as 'C = A & B'), users are usually more interested in structural errors.
//
// - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection
// than to report that 'D' is not assignable to 'A' or 'B'.
//
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
// breaking the intersection apart.
if (result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false)) {
return result;
}
else if (target.flags & TypeFlags.Intersection) {
if (result = typeRelatedToEachType(source, target as IntersectionType, reportErrors)) {
return result;
}
if (target.flags & TypeFlags.Union) {
if (result = typeRelatedToSomeType(source, <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive))) {
return result;
}
}
else if (source.flags & TypeFlags.Intersection) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not check the source first here too? I think that's the normal pattern for isRelatedTo

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above the initial if statement explains why. For intersection types, the target side is an "each" relationship and the source side is a "some" relationship. We need to deconstruct "each" relationships first to get the correct results.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so I think I misunderstood the parts of the code that are independent post-distribution. It would work to put intersections before unions now, where it wouldn't before, right?

// Check to see if any constituents of the intersection are immediately related to the target.
//
// Don't report errors though. Checking whether a constituent is related to the source is not actually
// useful and leads to some confusing error messages. Instead it is better to let the below checks
// take care of this, or to not elaborate at all. For instance,
//
// - For an object type (such as 'C = A & B'), users are usually more interested in structural errors.
//
// - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection
// than to report that 'D' is not assignable to 'A' or 'B'.
//
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
// breaking the intersection apart.
if (result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false)) {
return result;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,12 @@ namespace ts {
: undefined;
}

export function replaceElement<T>(array: T[], index: number, value: T): T[] {
const result = array.slice(0);
result[index] = value;
return result;
}

/**
* Performs a binary search, finding the index at which 'value' occurs in 'array'.
* If no such index is found, returns the 2's-complement of first index at which
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(17,5): error TS2322: Type 'A & B' is not assignable to type 'number'.
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(18,5): error TS2322: Type 'A & B' is not assignable to type 'boolean'.
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(19,5): error TS2322: Type 'A & B' is not assignable to type 'string'.
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(21,5): error TS2322: Type 'number & boolean' is not assignable to type 'string'.
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(21,5): error TS2322: Type '(number & true) | (number & false)' is not assignable to type 'string'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some way to present this error as number & boolean still? I feel like the distributed form is unexpected here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual representation needs to be (number & true) | (number & false), but we could play tricks in the type-to-string logic to turn it into number & boolean. Not sure it's worth it though as those types are all rather meaningless anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, type-to-string is what I was thinking of. But you're right, & boolean probably only shows up in our tests.

Type 'number & true' is not assignable to type 'string'.


==== tests/cases/compiler/errorMessagesIntersectionTypes04.ts (4 errors) ====
Expand Down Expand Up @@ -33,5 +34,6 @@ tests/cases/compiler/errorMessagesIntersectionTypes04.ts(21,5): error TS2322: Ty

str = num_and_bool;
~~~
!!! error TS2322: Type 'number & boolean' is not assignable to type 'string'.
!!! error TS2322: Type '(number & true) | (number & false)' is not assignable to type 'string'.
!!! error TS2322: Type 'number & true' is not assignable to type 'string'.
}
74 changes: 38 additions & 36 deletions tests/baselines/reference/intersectionAndUnionTypes.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,29 @@ tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(29,1): e
Type 'A & B' is not assignable to type 'C | D'.
Type 'A & B' is not assignable to type 'D'.
Property 'd' is missing in type 'A & B'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(31,1): error TS2322: Type 'A & B' is not assignable to type '(A | B) & (C | D)'.
Type 'A & B' is not assignable to type 'C | D'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(31,1): error TS2322: Type 'A & B' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
Type 'A & B' is not assignable to type 'B & D'.
Type 'A & B' is not assignable to type 'D'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(32,1): error TS2322: Type 'A | B' is not assignable to type '(A | B) & (C | D)'.
Type 'A' is not assignable to type '(A | B) & (C | D)'.
Type 'A' is not assignable to type 'C | D'.
Type 'A' is not assignable to type 'D'.
Property 'd' is missing in type 'A'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(33,1): error TS2322: Type 'C & D' is not assignable to type '(A | B) & (C | D)'.
Type 'C & D' is not assignable to type 'A | B'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(32,1): error TS2322: Type 'A | B' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
Type 'A' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
Type 'A' is not assignable to type 'B & D'.
Type 'A' is not assignable to type 'B'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(33,1): error TS2322: Type 'C & D' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
Type 'C & D' is not assignable to type 'B & D'.
Type 'C & D' is not assignable to type 'B'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(34,1): error TS2322: Type 'C | D' is not assignable to type '(A | B) & (C | D)'.
Type 'C' is not assignable to type '(A | B) & (C | D)'.
Type 'C' is not assignable to type 'A | B'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(34,1): error TS2322: Type 'C | D' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
Type 'C' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
Type 'C' is not assignable to type 'B & D'.
Type 'C' is not assignable to type 'B'.
Property 'b' is missing in type 'C'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(35,1): error TS2322: Type '(A | B) & (C | D)' is not assignable to type 'A & B'.
Type '(A | B) & (C | D)' is not assignable to type 'A'.
Property 'a' is missing in type '(A | B) & (C | D)'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(37,1): error TS2322: Type '(A | B) & (C | D)' is not assignable to type 'C & D'.
Type '(A | B) & (C | D)' is not assignable to type 'C'.
Property 'c' is missing in type '(A | B) & (C | D)'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(35,1): error TS2322: Type '(A & C) | (A & D) | (B & C) | (B & D)' is not assignable to type 'A & B'.
Type 'A & C' is not assignable to type 'A & B'.
Type 'A & C' is not assignable to type 'B'.
Property 'b' is missing in type 'A & C'.
tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(37,1): error TS2322: Type '(A & C) | (A & D) | (B & C) | (B & D)' is not assignable to type 'C & D'.
Type 'A & C' is not assignable to type 'C & D'.
Type 'A & C' is not assignable to type 'D'.
Property 'd' is missing in type 'A & C'.


==== tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts (14 errors) ====
Expand Down Expand Up @@ -127,38 +128,39 @@ tests/cases/conformance/types/intersection/intersectionAndUnionTypes.ts(37,1): e

y = anb;
~
!!! error TS2322: Type 'A & B' is not assignable to type '(A | B) & (C | D)'.
!!! error TS2322: Type 'A & B' is not assignable to type 'C | D'.
!!! error TS2322: Type 'A & B' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
!!! error TS2322: Type 'A & B' is not assignable to type 'B & D'.
!!! error TS2322: Type 'A & B' is not assignable to type 'D'.
y = aob;
~
!!! error TS2322: Type 'A | B' is not assignable to type '(A | B) & (C | D)'.
!!! error TS2322: Type 'A' is not assignable to type '(A | B) & (C | D)'.
!!! error TS2322: Type 'A' is not assignable to type 'C | D'.
!!! error TS2322: Type 'A' is not assignable to type 'D'.
!!! error TS2322: Property 'd' is missing in type 'A'.
!!! error TS2322: Type 'A | B' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
!!! error TS2322: Type 'A' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
!!! error TS2322: Type 'A' is not assignable to type 'B & D'.
!!! error TS2322: Type 'A' is not assignable to type 'B'.
y = cnd;
~
!!! error TS2322: Type 'C & D' is not assignable to type '(A | B) & (C | D)'.
!!! error TS2322: Type 'C & D' is not assignable to type 'A | B'.
!!! error TS2322: Type 'C & D' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
!!! error TS2322: Type 'C & D' is not assignable to type 'B & D'.
!!! error TS2322: Type 'C & D' is not assignable to type 'B'.
y = cod;
~
!!! error TS2322: Type 'C | D' is not assignable to type '(A | B) & (C | D)'.
!!! error TS2322: Type 'C' is not assignable to type '(A | B) & (C | D)'.
!!! error TS2322: Type 'C' is not assignable to type 'A | B'.
!!! error TS2322: Type 'C | D' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
!!! error TS2322: Type 'C' is not assignable to type '(A & C) | (A & D) | (B & C) | (B & D)'.
!!! error TS2322: Type 'C' is not assignable to type 'B & D'.
!!! error TS2322: Type 'C' is not assignable to type 'B'.
!!! error TS2322: Property 'b' is missing in type 'C'.
anb = y;
~~~
!!! error TS2322: Type '(A | B) & (C | D)' is not assignable to type 'A & B'.
!!! error TS2322: Type '(A | B) & (C | D)' is not assignable to type 'A'.
!!! error TS2322: Property 'a' is missing in type '(A | B) & (C | D)'.
!!! error TS2322: Type '(A & C) | (A & D) | (B & C) | (B & D)' is not assignable to type 'A & B'.
!!! error TS2322: Type 'A & C' is not assignable to type 'A & B'.
!!! error TS2322: Type 'A & C' is not assignable to type 'B'.
!!! error TS2322: Property 'b' is missing in type 'A & C'.
aob = y; // Ok
cnd = y;
~~~
!!! error TS2322: Type '(A | B) & (C | D)' is not assignable to type 'C & D'.
!!! error TS2322: Type '(A | B) & (C | D)' is not assignable to type 'C'.
!!! error TS2322: Property 'c' is missing in type '(A | B) & (C | D)'.
!!! error TS2322: Type '(A & C) | (A & D) | (B & C) | (B & D)' is not assignable to type 'C & D'.
!!! error TS2322: Type 'A & C' is not assignable to type 'C & D'.
!!! error TS2322: Type 'A & C' is not assignable to type 'D'.
!!! error TS2322: Property 'd' is missing in type 'A & C'.
cod = y; // Ok

79 changes: 79 additions & 0 deletions tests/baselines/reference/intersectionTypeNormalization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//// [intersectionTypeNormalization.ts]
interface A { a: string }
interface B { b: string }
interface C { c: string }
interface D { d: string }

// Identical ways of writing the same type
type X1 = (A | B) & (C | D);
type X2 = A & (C | D) | B & (C | D)
type X3 = A & C | A & D | B & C | B & D;

var x: X1;
var x: X2;
var x: X3;

interface X { x: string }
interface Y { y: string }

// Identical ways of writing the same type
type Y1 = (A | X & Y) & (C | D);
type Y2 = A & (C | D) | X & Y & (C | D)
type Y3 = A & C | A & D | X & Y & C | X & Y & D;

var y: Y1;
var y: Y2;
var y: Y3;

interface M { m: string }
interface N { n: string }

// Identical ways of writing the same type
type Z1 = (A | X & (M | N)) & (C | D);
type Z2 = A & (C | D) | X & (M | N) & (C | D)
type Z3 = A & C | A & D | X & (M | N) & C | X & (M | N) & D;
type Z4 = A & C | A & D | X & M & C | X & N & C | X & M & D | X & N & D;

var z: Z1;
var z: Z2;
var z: Z3;
var z: Z4;

// Repro from #9919

type ToString = {
toString(): string;
}

type BoxedValue = { kind: 'int', num: number }
| { kind: 'string', str: string }

type IntersectionFail = BoxedValue & ToString

type IntersectionInline = { kind: 'int', num: number } & ToString
| { kind: 'string', str: string } & ToString

function getValueAsString(value: IntersectionFail): string {
if (value.kind === 'int') {
return '' + value.num;
}
return value.str;
}

//// [intersectionTypeNormalization.js]
var x;
var x;
var x;
var y;
var y;
var y;
var z;
var z;
var z;
var z;
function getValueAsString(value) {
if (value.kind === 'int') {
return '' + value.num;
}
return value.str;
}
Loading