Skip to content

Commit 6c28da3

Browse files
authored
Merge pull request #23672 from Microsoft/intersectionWithUnionConstraint
Type relationships for intersections with union constraints
2 parents 5ecd03e + bbcb1bb commit 6c28da3

16 files changed

+506
-28
lines changed

src/compiler/checker.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6470,6 +6470,47 @@ namespace ts {
64706470
return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type);
64716471
}
64726472

6473+
function getUnionConstraintOfIntersection(type: IntersectionType, targetIsUnion: boolean) {
6474+
let constraints: Type[];
6475+
let hasDisjointDomainType = false;
6476+
for (const t of type.types) {
6477+
if (t.flags & TypeFlags.Instantiable) {
6478+
// We keep following constraints as long as we have an instantiable type that is known
6479+
// not to be circular or infinite (hence we stop on index access types).
6480+
let constraint = getConstraintOfType(t);
6481+
while (constraint && constraint.flags & (TypeFlags.TypeParameter | TypeFlags.Index | TypeFlags.Conditional)) {
6482+
constraint = getConstraintOfType(constraint);
6483+
}
6484+
if (constraint) {
6485+
// A constraint that isn't a union type implies that the final type would be a non-union
6486+
// type as well. Since non-union constraints are of no interest, we can exit here.
6487+
if (!(constraint.flags & TypeFlags.Union)) {
6488+
return undefined;
6489+
}
6490+
constraints = append(constraints, constraint);
6491+
}
6492+
}
6493+
else if (t.flags & TypeFlags.DisjointDomains) {
6494+
hasDisjointDomainType = true;
6495+
}
6496+
}
6497+
// If the target is a union type or if we are intersecting with types belonging to one of the
6498+
// disjoint domans, we may end up producing a constraint that hasn't been examined before.
6499+
if (constraints && (targetIsUnion || hasDisjointDomainType)) {
6500+
if (hasDisjointDomainType) {
6501+
// We add any types belong to one of the disjoint domans because they might cause the final
6502+
// intersection operation to reduce the union constraints.
6503+
for (const t of type.types) {
6504+
if (t.flags & TypeFlags.DisjointDomains) {
6505+
constraints = append(constraints, t);
6506+
}
6507+
}
6508+
}
6509+
return getIntersectionType(constraints);
6510+
}
6511+
return undefined;
6512+
}
6513+
64736514
function getBaseConstraintOfInstantiableNonPrimitiveUnionOrIntersection(type: Type) {
64746515
if (type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.UnionOrIntersection)) {
64756516
const constraint = getResolvedBaseConstraint(<InstantiableType | UnionOrIntersectionType>type);
@@ -7951,16 +7992,27 @@ namespace ts {
79517992
return binarySearch(types, type, getTypeId, compareValues) >= 0;
79527993
}
79537994

7954-
// Return true if the given intersection type contains (a) more than one unit type or (b) an object
7955-
// type and a nullable type (null or undefined).
7995+
// Return true if the given intersection type contains
7996+
// more than one unit type or,
7997+
// an object type and a nullable type (null or undefined), or
7998+
// a string-like type and a type known to be non-string-like, or
7999+
// a number-like type and a type known to be non-number-like, or
8000+
// a symbol-like type and a type known to be non-symbol-like, or
8001+
// a void-like type and a type known to be non-void-like, or
8002+
// a non-primitive type and a type known to be primitive.
79568003
function isEmptyIntersectionType(type: IntersectionType) {
79578004
let combined: TypeFlags = 0;
79588005
for (const t of type.types) {
79598006
if (t.flags & TypeFlags.Unit && combined & TypeFlags.Unit) {
79608007
return true;
79618008
}
79628009
combined |= t.flags;
7963-
if (combined & TypeFlags.Nullable && combined & (TypeFlags.Object | TypeFlags.NonPrimitive)) {
8010+
if (combined & TypeFlags.Nullable && combined & (TypeFlags.Object | TypeFlags.NonPrimitive) ||
8011+
combined & TypeFlags.NonPrimitive && combined & (TypeFlags.DisjointDomains & ~TypeFlags.NonPrimitive) ||
8012+
combined & TypeFlags.StringLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.StringLike) ||
8013+
combined & TypeFlags.NumberLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.NumberLike) ||
8014+
combined & TypeFlags.ESSymbolLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.ESSymbolLike) ||
8015+
combined & TypeFlags.VoidLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.VoidLike)) {
79648016
return true;
79658017
}
79668018
}
@@ -10180,6 +10232,23 @@ namespace ts {
1018010232
}
1018110233
}
1018210234
}
10235+
if (!result && source.flags & TypeFlags.Intersection) {
10236+
// The combined constraint of an intersection type is the intersection of the constraints of
10237+
// the constituents. When an intersection type contains instantiable types with union type
10238+
// constraints, there are situations where we need to examine the combined constraint. One is
10239+
// when the target is a union type. Another is when the intersection contains types belonging
10240+
// to one of the disjoint domains. For example, given type variables T and U, each with the
10241+
// constraint 'string | number', the combined constraint of 'T & U' is 'string | number' and
10242+
// we need to check this constraint against a union on the target side. Also, given a type
10243+
// variable V constrained to 'string | number', 'V & number' has a combined constraint of
10244+
// 'string & number | number & number' which reduces to just 'number'.
10245+
const constraint = getUnionConstraintOfIntersection(<IntersectionType>source, !!(target.flags & TypeFlags.Union));
10246+
if (constraint) {
10247+
if (result = isRelatedTo(constraint, target, reportErrors)) {
10248+
errorInfo = saveErrorInfo;
10249+
}
10250+
}
10251+
}
1018310252

1018410253
isIntersectionConstituent = saveIsIntersectionConstituent;
1018510254

src/compiler/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3620,6 +3620,9 @@ namespace ts {
36203620
BooleanLike = Boolean | BooleanLiteral,
36213621
EnumLike = Enum | EnumLiteral,
36223622
ESSymbolLike = ESSymbol | UniqueESSymbol,
3623+
VoidLike = Void | Undefined,
3624+
/* @internal */
3625+
DisjointDomains = NonPrimitive | StringLike | NumberLike | BooleanLike | ESSymbolLike | VoidLike | Null,
36233626
UnionOrIntersection = Union | Intersection,
36243627
StructuredType = Object | Union | Intersection,
36253628
TypeVariable = TypeParameter | IndexedAccess,

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,7 @@ declare namespace ts {
21112111
BooleanLike = 136,
21122112
EnumLike = 272,
21132113
ESSymbolLike = 1536,
2114+
VoidLike = 6144,
21142115
UnionOrIntersection = 393216,
21152116
StructuredType = 458752,
21162117
TypeVariable = 1081344,

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,7 @@ declare namespace ts {
21112111
BooleanLike = 136,
21122112
EnumLike = 272,
21132113
ESSymbolLike = 1536,
2114+
VoidLike = 6144,
21142115
UnionOrIntersection = 393216,
21152116
StructuredType = 458752,
21162117
TypeVariable = 1081344,

tests/baselines/reference/errorMessagesIntersectionTypes04.errors.txt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(17,5): error TS2322: Type 'A & B' is not assignable to type 'number'.
22
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(18,5): error TS2322: Type 'A & B' is not assignable to type 'boolean'.
33
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(19,5): error TS2322: Type 'A & B' is not assignable to type 'string'.
4-
tests/cases/compiler/errorMessagesIntersectionTypes04.ts(21,5): error TS2322: Type '(number & true) | (number & false)' is not assignable to type 'string'.
5-
Type 'number & true' is not assignable to type 'string'.
64

75

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

3533
str = num_and_bool;
36-
~~~
37-
!!! error TS2322: Type '(number & true) | (number & false)' is not assignable to type 'string'.
38-
!!! error TS2322: Type 'number & true' is not assignable to type 'string'.
3934
}

tests/baselines/reference/errorMessagesIntersectionTypes04.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function f<T, U extends A, V extends U>(): void {
3636
>B : B
3737

3838
let num_and_bool: number & boolean;
39-
>num_and_bool : (number & true) | (number & false)
39+
>num_and_bool : never
4040

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

5656
str = num_and_bool;
57-
>str = num_and_bool : (number & true) | (number & false)
57+
>str = num_and_bool : never
5858
>str : string
59-
>num_and_bool : (number & true) | (number & false)
59+
>num_and_bool : never
6060
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(7,9): error TS2322: Type 'T & U' is not assignable to type 'string | number'.
2+
Type 'string | undefined' is not assignable to type 'string | number'.
3+
Type 'undefined' is not assignable to type 'string | number'.
4+
Type 'T & U' is not assignable to type 'number'.
5+
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(8,9): error TS2322: Type 'T & U' is not assignable to type 'string | null'.
6+
Type 'string | undefined' is not assignable to type 'string | null'.
7+
Type 'undefined' is not assignable to type 'string | null'.
8+
Type 'T & U' is not assignable to type 'string'.
9+
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(10,9): error TS2322: Type 'T & U' is not assignable to type 'number | null'.
10+
Type 'string | undefined' is not assignable to type 'number | null'.
11+
Type 'undefined' is not assignable to type 'number | null'.
12+
Type 'T & U' is not assignable to type 'number'.
13+
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(11,9): error TS2322: Type 'T & U' is not assignable to type 'number | undefined'.
14+
Type 'string | undefined' is not assignable to type 'number | undefined'.
15+
Type 'string' is not assignable to type 'number | undefined'.
16+
Type 'T & U' is not assignable to type 'number'.
17+
tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts(12,9): error TS2322: Type 'T & U' is not assignable to type 'null | undefined'.
18+
Type 'string | undefined' is not assignable to type 'null | undefined'.
19+
Type 'string' is not assignable to type 'null | undefined'.
20+
Type 'T & U' is not assignable to type 'null'.
21+
22+
23+
==== tests/cases/conformance/types/intersection/intersectionWithUnionConstraint.ts (5 errors) ====
24+
function f1<T extends string | number, U extends string | number>(x: T & U) {
25+
// Combined constraint of 'T & U' is 'string | number'
26+
let y: string | number = x;
27+
}
28+
29+
function f2<T extends string | number | undefined, U extends string | null | undefined>(x: T & U) {
30+
let y1: string | number = x; // Error
31+
~~
32+
!!! error TS2322: Type 'T & U' is not assignable to type 'string | number'.
33+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string | number'.
34+
!!! error TS2322: Type 'undefined' is not assignable to type 'string | number'.
35+
!!! error TS2322: Type 'T & U' is not assignable to type 'number'.
36+
let y2: string | null = x; // Error
37+
~~
38+
!!! error TS2322: Type 'T & U' is not assignable to type 'string | null'.
39+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string | null'.
40+
!!! error TS2322: Type 'undefined' is not assignable to type 'string | null'.
41+
!!! error TS2322: Type 'T & U' is not assignable to type 'string'.
42+
let y3: string | undefined = x;
43+
let y4: number | null = x; // Error
44+
~~
45+
!!! error TS2322: Type 'T & U' is not assignable to type 'number | null'.
46+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'number | null'.
47+
!!! error TS2322: Type 'undefined' is not assignable to type 'number | null'.
48+
!!! error TS2322: Type 'T & U' is not assignable to type 'number'.
49+
let y5: number | undefined = x; // Error
50+
~~
51+
!!! error TS2322: Type 'T & U' is not assignable to type 'number | undefined'.
52+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'number | undefined'.
53+
!!! error TS2322: Type 'string' is not assignable to type 'number | undefined'.
54+
!!! error TS2322: Type 'T & U' is not assignable to type 'number'.
55+
let y6: null | undefined = x; // Error
56+
~~
57+
!!! error TS2322: Type 'T & U' is not assignable to type 'null | undefined'.
58+
!!! error TS2322: Type 'string | undefined' is not assignable to type 'null | undefined'.
59+
!!! error TS2322: Type 'string' is not assignable to type 'null | undefined'.
60+
!!! error TS2322: Type 'T & U' is not assignable to type 'null'.
61+
}
62+
63+
type T1 = (string | number | undefined) & (string | null | undefined); // string | undefined
64+
65+
function f3<T extends string | number | undefined>(x: T & (number | object | undefined)) {
66+
const y: number | undefined = x;
67+
}
68+
69+
function f4<T extends string | number>(x: T & (number | object)) {
70+
const y: number = x;
71+
}
72+
73+
function f5<T, U extends keyof T>(x: keyof T & U) {
74+
let y: keyof any = x;
75+
}
76+
77+
// Repro from #23648
78+
79+
type Example<T, U> = { [K in keyof T]: K extends keyof U ? UnexpectedError<K> : NoErrorHere<K> }
80+
81+
type UnexpectedError<T extends PropertyKey> = T
82+
type NoErrorHere<T extends PropertyKey> = T
83+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//// [intersectionWithUnionConstraint.ts]
2+
function f1<T extends string | number, U extends string | number>(x: T & U) {
3+
// Combined constraint of 'T & U' is 'string | number'
4+
let y: string | number = x;
5+
}
6+
7+
function f2<T extends string | number | undefined, U extends string | null | undefined>(x: T & U) {
8+
let y1: string | number = x; // Error
9+
let y2: string | null = x; // Error
10+
let y3: string | undefined = x;
11+
let y4: number | null = x; // Error
12+
let y5: number | undefined = x; // Error
13+
let y6: null | undefined = x; // Error
14+
}
15+
16+
type T1 = (string | number | undefined) & (string | null | undefined); // string | undefined
17+
18+
function f3<T extends string | number | undefined>(x: T & (number | object | undefined)) {
19+
const y: number | undefined = x;
20+
}
21+
22+
function f4<T extends string | number>(x: T & (number | object)) {
23+
const y: number = x;
24+
}
25+
26+
function f5<T, U extends keyof T>(x: keyof T & U) {
27+
let y: keyof any = x;
28+
}
29+
30+
// Repro from #23648
31+
32+
type Example<T, U> = { [K in keyof T]: K extends keyof U ? UnexpectedError<K> : NoErrorHere<K> }
33+
34+
type UnexpectedError<T extends PropertyKey> = T
35+
type NoErrorHere<T extends PropertyKey> = T
36+
37+
38+
//// [intersectionWithUnionConstraint.js]
39+
"use strict";
40+
function f1(x) {
41+
// Combined constraint of 'T & U' is 'string | number'
42+
var y = x;
43+
}
44+
function f2(x) {
45+
var y1 = x; // Error
46+
var y2 = x; // Error
47+
var y3 = x;
48+
var y4 = x; // Error
49+
var y5 = x; // Error
50+
var y6 = x; // Error
51+
}
52+
function f3(x) {
53+
var y = x;
54+
}
55+
function f4(x) {
56+
var y = x;
57+
}
58+
function f5(x) {
59+
var y = x;
60+
}

0 commit comments

Comments
 (0)