Skip to content

Type relationships for intersections with union constraints #23672

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

Merged
merged 9 commits into from
Apr 27, 2018
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
75 changes: 72 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6428,6 +6428,47 @@ namespace ts {
return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type);
}

function getUnionConstraintOfIntersection(type: IntersectionType, targetIsUnion: boolean) {
let constraints: Type[];
let hasDisjointDomainType = false;
for (const t of type.types) {
if (t.flags & TypeFlags.Instantiable) {
// We keep following constraints as long as we have an instantiable type that is known
// not to be circular or infinite (hence we stop on index access types).
let constraint = getConstraintOfType(t);
while (constraint && constraint.flags & (TypeFlags.TypeParameter | TypeFlags.Index | TypeFlags.Conditional)) {
constraint = getConstraintOfType(constraint);
}
if (constraint) {
// A constraint that isn't a union type implies that the final type would be a non-union
// type as well. Since non-union constraints are of no interest, we can exit here.
if (!(constraint.flags & TypeFlags.Union)) {
return undefined;
}
constraints = append(constraints, constraint);
}
}
else if (t.flags & TypeFlags.DisjointDomains) {
hasDisjointDomainType = true;
}
}
// If the target is a union type or if we are intersecting with types belonging to one of the
// disjoint domans, we may end up producing a constraint that hasn't been examined before.
if (constraints && (targetIsUnion || hasDisjointDomainType)) {
if (hasDisjointDomainType) {
// We add any types belong to one of the disjoint domans because they might cause the final
// intersection operation to reduce the union constraints.
for (const t of type.types) {
if (t.flags & TypeFlags.DisjointDomains) {
constraints = append(constraints, t);
}
}
}
return getIntersectionType(constraints);
}
return undefined;
}

function getBaseConstraintOfInstantiableNonPrimitiveUnionOrIntersection(type: Type) {
if (type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.UnionOrIntersection)) {
const constraint = getResolvedBaseConstraint(<InstantiableType | UnionOrIntersectionType>type);
Expand Down Expand Up @@ -7897,16 +7938,27 @@ namespace ts {
return binarySearch(types, type, getTypeId, compareValues) >= 0;
}

// Return true if the given intersection type contains (a) more than one unit type or (b) an object
// type and a nullable type (null or undefined).
// Return true if the given intersection type contains
// more than one unit type or,
// an object type and a nullable type (null or undefined), or
// a string-like type and a type known to be non-string-like, or
// a number-like type and a type known to be non-number-like, or
// a symbol-like type and a type known to be non-symbol-like, or
// a void-like type and a type known to be non-void-like, or
// a non-primitive type and a type known to be primitive.
function isEmptyIntersectionType(type: IntersectionType) {
let combined: TypeFlags = 0;
for (const t of type.types) {
if (t.flags & TypeFlags.Unit && combined & TypeFlags.Unit) {
return true;
}
combined |= t.flags;
if (combined & TypeFlags.Nullable && combined & (TypeFlags.Object | TypeFlags.NonPrimitive)) {
if (combined & TypeFlags.Nullable && combined & (TypeFlags.Object | TypeFlags.NonPrimitive) ||
combined & TypeFlags.NonPrimitive && combined & (TypeFlags.DisjointDomains & ~TypeFlags.NonPrimitive) ||
combined & TypeFlags.StringLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.StringLike) ||
combined & TypeFlags.NumberLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.NumberLike) ||
combined & TypeFlags.ESSymbolLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.ESSymbolLike) ||
combined & TypeFlags.VoidLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.VoidLike)) {
return true;
}
}
Expand Down Expand Up @@ -10126,6 +10178,23 @@ namespace ts {
}
}
}
if (!result && source.flags & TypeFlags.Intersection) {
// The combined constraint of an intersection type is the intersection of the constraints of
// the constituents. When an intersection type contains instantiable types with union type
// constraints, there are situations where we need to examine the combined constraint. One is
// when the target is a union type. Another is when the intersection contains types belonging
// to one of the disjoint domains. For example, given type variables T and U, each with the
// constraint 'string | number', the combined constraint of 'T & U' is 'string | number' and
// we need to check this constraint against a union on the target side. Also, given a type
// variable V constrained to 'string | number', 'V & number' has a combined constraint of
// 'string & number | number & number' which reduces to just 'number'.
const constraint = getUnionConstraintOfIntersection(<IntersectionType>source, !!(target.flags & TypeFlags.Union));
if (constraint) {
if (result = isRelatedTo(constraint, target, reportErrors)) {
errorInfo = saveErrorInfo;
}
}
}

isIntersectionConstituent = saveIsIntersectionConstituent;

Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3613,6 +3613,9 @@ namespace ts {
BooleanLike = Boolean | BooleanLiteral,
EnumLike = Enum | EnumLiteral,
ESSymbolLike = ESSymbol | UniqueESSymbol,
VoidLike = Void | Undefined,
Copy link
Member

Choose a reason for hiding this comment

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

Ahhh.... can we go with the term UndefinedLike instead? Since void is a TS-only and, conceptually, corresponds with a function return of no value, which maps to a runtime value of undefined (whereas undefined doesn't really imply anything void-y on its own).

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure. I went with void-like because void is the supertype, but either way works.

/* @internal */
DisjointDomains = NonPrimitive | StringLike | NumberLike | BooleanLike | ESSymbolLike | VoidLike | Null,
Copy link
Member

Choose a reason for hiding this comment

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

Object types can also have disjoint domains, eg

type A = { x: string };
type B = { x: number };
type C = { x: boolean };
function f5<T extends A | B>(x: T & (B | C)) {
    const y: B = x; // expected to work, currently does not, doesn't work with this PR
}

(important if you just consider each of those types of x as different tags for tagged unions, eg "a", "b", and "c")

What we have here is certainly better, but definitely still not quite complete.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, the intent here is simply to do better for the primitive types. I don't think it is feasible to reason about disjoint domains for arbitrary object types since they can be recursive or infinite. Also, we still allow stuff like string & { __tag__?: void } even though that's technically an impossible type.

Copy link
Member

Choose a reason for hiding this comment

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

Also, we still allow stuff like string & { tag?: void } even though that's technically an impossible type.

I feel like you can justify that by claiming that {} types indicate structure, but no top level domain (which is mostly true - { toString(): string } matches primitives AFAIK), so it's just string-augmented-with-extras. So it doesn't feel so bad.

Also, it's probably feasible for object types - we do compare them based on that structure, after all, and we're even within that comparison when this simplifier/inliner gets called. And it's not like { x: number } & { x: string } should simplify to never - it should be { x: never } (and { x: number } | { x: never } should be { x: number }) - it's exactly what we'd do when we actually compare the properties (or asked for obj.x), but not deferred until access so that the top-level union/intersection relationship works out (and this is all about doing union/intersection simplification more eagerly to make comparisons work, so it kinda fits right in).

I don't suppose we need to fix it as part of this (after all, it's already better than master), but we should at least add the test and know that the fix is incomplete, so we can either revisit it or keep its behavior stable.

UnionOrIntersection = Union | Intersection,
StructuredType = Object | Union | Intersection,
TypeVariable = TypeParameter | IndexedAccess,
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2105,6 +2105,7 @@ declare namespace ts {
BooleanLike = 136,
EnumLike = 272,
ESSymbolLike = 1536,
VoidLike = 6144,
UnionOrIntersection = 393216,
StructuredType = 458752,
TypeVariable = 1081344,
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2105,6 +2105,7 @@ declare namespace ts {
BooleanLike = 136,
EnumLike = 272,
ESSymbolLike = 1536,
VoidLike = 6144,
UnionOrIntersection = 393216,
StructuredType = 458752,
TypeVariable = 1081344,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
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 & true) | (number & false)' is not assignable to type 'string'.
Type 'number & true' is not assignable to type 'string'.


==== tests/cases/compiler/errorMessagesIntersectionTypes04.ts (4 errors) ====
==== tests/cases/compiler/errorMessagesIntersectionTypes04.ts (3 errors) ====
interface A {
a;
}
Expand Down Expand Up @@ -33,7 +31,4 @@ tests/cases/compiler/errorMessagesIntersectionTypes04.ts(21,5): error TS2322: Ty
!!! error TS2322: Type 'A & B' is not assignable to type 'string'.

str = num_and_bool;
~~~
!!! error TS2322: Type '(number & true) | (number & false)' is not assignable to type 'string'.
!!! error TS2322: Type 'number & true' is not assignable to type 'string'.
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function f<T, U extends A, V extends U>(): void {
>B : B

let num_and_bool: number & boolean;
>num_and_bool : (number & true) | (number & false)
>num_and_bool : never

num = a_and_b;
>num = a_and_b : A & B
Expand All @@ -54,7 +54,7 @@ function f<T, U extends A, V extends U>(): void {
>a_and_b : A & B

str = num_and_bool;
>str = num_and_bool : (number & true) | (number & false)
>str = num_and_bool : never
>str : string
>num_and_bool : (number & true) | (number & false)
>num_and_bool : never
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(7,9): error TS2322: Type 'T & U' is not assignable to type 'string | number'.
Type 'string | undefined' is not assignable to type 'string | number'.
Type 'undefined' is not assignable to type 'string | number'.
Type 'T & U' is not assignable to type 'number'.
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(8,9): error TS2322: Type 'T & U' is not assignable to type 'string | null'.
Type 'string | undefined' is not assignable to type 'string | null'.
Type 'undefined' is not assignable to type 'string | null'.
Type 'T & U' is not assignable to type 'string'.
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(10,9): error TS2322: Type 'T & U' is not assignable to type 'number | null'.
Type 'string | undefined' is not assignable to type 'number | null'.
Type 'undefined' is not assignable to type 'number | null'.
Type 'T & U' is not assignable to type 'number'.
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(11,9): error TS2322: Type 'T & U' is not assignable to type 'number | undefined'.
Type 'string | undefined' is not assignable to type 'number | undefined'.
Type 'string' is not assignable to type 'number | undefined'.
Type 'T & U' is not assignable to type 'number'.
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(12,9): error TS2322: Type 'T & U' is not assignable to type 'null | undefined'.
Type 'string | undefined' is not assignable to type 'null | undefined'.
Type 'string' is not assignable to type 'null | undefined'.
Type 'T & U' is not assignable to type 'null'.


==== tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts (5 errors) ====
function f1<T extends string | number, U extends string | number>(x: T & U) {
// Combined constraint of 'T & U' is 'string | number'
let y: string | number = x;
}

function f2<T extends string | number | undefined, U extends string | null | undefined>(x: T & U) {
let y1: string | number = x; // Error
~~
!!! error TS2322: Type 'T & U' is not assignable to type 'string | number'.
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string | number'.
!!! error TS2322: Type 'undefined' is not assignable to type 'string | number'.
!!! error TS2322: Type 'T & U' is not assignable to type 'number'.
let y2: string | null = x; // Error
~~
!!! error TS2322: Type 'T & U' is not assignable to type 'string | null'.
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string | null'.
!!! error TS2322: Type 'undefined' is not assignable to type 'string | null'.
!!! error TS2322: Type 'T & U' is not assignable to type 'string'.
let y3: string | undefined = x;
let y4: number | null = x; // Error
~~
!!! error TS2322: Type 'T & U' is not assignable to type 'number | null'.
!!! error TS2322: Type 'string | undefined' is not assignable to type 'number | null'.
!!! error TS2322: Type 'undefined' is not assignable to type 'number | null'.
!!! error TS2322: Type 'T & U' is not assignable to type 'number'.
let y5: number | undefined = x; // Error
~~
!!! error TS2322: Type 'T & U' is not assignable to type 'number | undefined'.
!!! error TS2322: Type 'string | undefined' is not assignable to type 'number | undefined'.
!!! error TS2322: Type 'string' is not assignable to type 'number | undefined'.
!!! error TS2322: Type 'T & U' is not assignable to type 'number'.
let y6: null | undefined = x; // Error
~~
!!! error TS2322: Type 'T & U' is not assignable to type 'null | undefined'.
!!! error TS2322: Type 'string | undefined' is not assignable to type 'null | undefined'.
!!! error TS2322: Type 'string' is not assignable to type 'null | undefined'.
!!! error TS2322: Type 'T & U' is not assignable to type 'null'.
}

type T1 = (string | number | undefined) & (string | null | undefined); // string | undefined

function f3<T extends string | number | undefined>(x: T & (number | object | undefined)) {
const y: number | undefined = x;
}

function f4<T extends string | number>(x: T & (number | object)) {
const y: number = x;
}

function f5<T, U extends keyof T>(x: keyof T & U) {
let y: keyof any = x;
}

// Repro from #23648

type Example<T, U> = { [K in keyof T]: K extends keyof U ? UnexpectedError<K> : NoErrorHere<K> }

type UnexpectedError<T extends PropertyKey> = T
type NoErrorHere<T extends PropertyKey> = T

60 changes: 60 additions & 0 deletions tests/baselines/reference/intersectionWithUnionConstraint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//// [intersectionWithUnionConstraint.ts]
function f1<T extends string | number, U extends string | number>(x: T & U) {
// Combined constraint of 'T & U' is 'string | number'
let y: string | number = x;
}

function f2<T extends string | number | undefined, U extends string | null | undefined>(x: T & U) {
let y1: string | number = x; // Error
let y2: string | null = x; // Error
let y3: string | undefined = x;
let y4: number | null = x; // Error
let y5: number | undefined = x; // Error
let y6: null | undefined = x; // Error
}

type T1 = (string | number | undefined) & (string | null | undefined); // string | undefined

function f3<T extends string | number | undefined>(x: T & (number | object | undefined)) {
const y: number | undefined = x;
}

function f4<T extends string | number>(x: T & (number | object)) {
const y: number = x;
}

function f5<T, U extends keyof T>(x: keyof T & U) {
let y: keyof any = x;
}

// Repro from #23648

type Example<T, U> = { [K in keyof T]: K extends keyof U ? UnexpectedError<K> : NoErrorHere<K> }

type UnexpectedError<T extends PropertyKey> = T
type NoErrorHere<T extends PropertyKey> = T


//// [intersectionWithUnionConstraint.js]
"use strict";
function f1(x) {
// Combined constraint of 'T & U' is 'string | number'
var y = x;
}
function f2(x) {
var y1 = x; // Error
var y2 = x; // Error
var y3 = x;
var y4 = x; // Error
var y5 = x; // Error
var y6 = x; // Error
}
function f3(x) {
var y = x;
}
function f4(x) {
var y = x;
}
function f5(x) {
var y = x;
}
Loading