Skip to content

Commit be4b814

Browse files
authored
Reduce intersections by discriminants (#36696)
* Treat never-like intersections as never * Accept new baselines * Fix compiler issues revealed by increased intersection correctness * Delete fourslash tests that are no longer applicable * Include isNeverLikeIntersection check in getNormalizedType * Erase never-like types in several more places * Check that base types are not never-like * Add comments * Revert isNeverLikeType check in getIndexType (keyof shouldn't resolve member types) * Introduce getReducedType for union and intersection types * Don't reduce in getApparentType * Avoid relationship check in resolveMappedTypeMembers * Accept new baselines * Don't call getReducedType in getIndexType * Ensure reduced and unreduced forms of a type can compare identical * Reduce types before converting them to string representation * Accept new baselines * Reduce intersections before obtaining keyof X * Add tests * Accept new baselines * Fix comment in tests * Don't infer from empty intersection types * Add tests * Accept new baselines * Defer instantiation of mapped type property types * Accept new baselines * Include more precise type in diagnostic * Accept new baselines * Minor optimization * Improve error message * Optional properties in intersections are never discriminants
1 parent f31ff2d commit be4b814

32 files changed

+1231
-189
lines changed

src/compiler/checker.ts

Lines changed: 112 additions & 36 deletions
Large diffs are not rendered by default.

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2365,6 +2365,10 @@
23652365
"category": "Error",
23662366
"code": 2614
23672367
},
2368+
"Type of property '{0}' circularly references itself in mapped type '{1}'.": {
2369+
"category": "Error",
2370+
"code": 2615
2371+
},
23682372

23692373
"Cannot augment module '{0}' with value exports because it resolves to a non-module entity.": {
23702374
"category": "Error",

src/compiler/factory.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,9 +1795,7 @@ namespace ts {
17951795

17961796
const target = getTargetOfBindingOrAssignmentElement(bindingElement);
17971797
if (target && isPropertyName(target)) {
1798-
return isComputedPropertyName(target) && isStringOrNumericLiteral(target.expression)
1799-
? target.expression
1800-
: target;
1798+
return target;
18011799
}
18021800
}
18031801

src/compiler/transformers/classFields.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ namespace ts {
976976
);
977977
}
978978

979-
function visitArrayAssignmentTarget(node: AssignmentPattern) {
979+
function visitArrayAssignmentTarget(node: BindingOrAssignmentElement) {
980980
const target = getTargetOfBindingOrAssignmentElement(node);
981981
if (target && isPrivateIdentifierPropertyAccessExpression(target)) {
982982
const wrapped = wrapPrivateIdentifierForDestructuringTarget(target);

src/compiler/types.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4155,6 +4155,9 @@ namespace ts {
41554155
OptionalParameter = 1 << 14, // Optional parameter
41564156
RestParameter = 1 << 15, // Rest parameter
41574157
DeferredType = 1 << 16, // Calculation of the type of this symbol is deferred due to processing costs, should be fetched with `getTypeOfSymbolWithDeferredType`
4158+
HasNeverType = 1 << 17, // Synthetic property with at least one never type in constituents
4159+
Mapped = 1 << 18, // Property of mapped type
4160+
StripOptional = 1 << 19, // Strip optionality in mapped property
41584161
Synthetic = SyntheticProperty | SyntheticMethod,
41594162
Discriminant = HasNonUniformType | HasLiteralType,
41604163
Partial = ReadPartial | WritePartial
@@ -4165,6 +4168,12 @@ namespace ts {
41654168
checkFlags: CheckFlags;
41664169
}
41674170

4171+
/* @internal */
4172+
export interface MappedSymbol extends TransientSymbol {
4173+
mappedType: MappedType;
4174+
mapper: TypeMapper;
4175+
}
4176+
41684177
/* @internal */
41694178
export interface ReverseMappedSymbol extends TransientSymbol {
41704179
propertyType: Type;
@@ -4362,16 +4371,16 @@ namespace ts {
43624371
NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable,
43634372
// The following flags are aggregated during union and intersection type construction
43644373
/* @internal */
4365-
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | NonPrimitive,
4374+
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive,
43664375
// The following flags are used for different purposes during union and intersection type construction
43674376
/* @internal */
43684377
IncludesStructuredOrInstantiable = TypeParameter,
43694378
/* @internal */
4370-
IncludesNonWideningType = Intersection,
4379+
IncludesNonWideningType = Index,
43714380
/* @internal */
4372-
IncludesWildcard = Index,
4381+
IncludesWildcard = IndexedAccess,
43734382
/* @internal */
4374-
IncludesEmptyObject = IndexedAccess,
4383+
IncludesEmptyObject = Conditional,
43754384
}
43764385

43774386
export type DestructuringPattern = BindingPattern | ObjectLiteralExpression | ArrayLiteralExpression;
@@ -4487,6 +4496,12 @@ namespace ts {
44874496
CouldContainTypeVariablesComputed = 1 << 26, // CouldContainTypeVariables flag has been computed
44884497
/* @internal */
44894498
CouldContainTypeVariables = 1 << 27, // Type could contain a type variable
4499+
/* @internal */
4500+
ContainsIntersections = 1 << 28, // Union contains intersections
4501+
/* @internal */
4502+
IsNeverIntersectionComputed = 1 << 28, // IsNeverLike flag has been computed
4503+
/* @internal */
4504+
IsNeverIntersection = 1 << 29, // Intersection reduces to never
44904505
ClassOrInterface = Class | Interface,
44914506
/* @internal */
44924507
RequiresWidening = ContainsWideningType | ContainsObjectOrArrayLiteral,
@@ -4608,6 +4623,8 @@ namespace ts {
46084623
}
46094624

46104625
export interface UnionType extends UnionOrIntersectionType {
4626+
/* @internal */
4627+
resolvedReducedType: Type;
46114628
}
46124629

46134630
export interface IntersectionType extends UnionOrIntersectionType {

tests/baselines/reference/discriminatedUnionTypes2.errors.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ tests/cases/conformance/types/union/discriminatedUnionTypes2.ts(27,30): error TS
22
Object literal may only specify known properties, and 'c' does not exist in type '{ a: null; b: string; }'.
33
tests/cases/conformance/types/union/discriminatedUnionTypes2.ts(32,11): error TS2339: Property 'b' does not exist on type '{ a: 0; b: string; } | { a: T; c: number; }'.
44
Property 'b' does not exist on type '{ a: T; c: number; }'.
5-
tests/cases/conformance/types/union/discriminatedUnionTypes2.ts(132,11): error TS2339: Property 'value' does not exist on type 'never'.
65

76

8-
==== tests/cases/conformance/types/union/discriminatedUnionTypes2.ts (3 errors) ====
7+
==== tests/cases/conformance/types/union/discriminatedUnionTypes2.ts (2 errors) ====
98
function f10(x : { kind: false, a: string } | { kind: true, b: string } | { kind: string, c: string }) {
109
if (x.kind === false) {
1110
x.a;
@@ -143,9 +142,7 @@ tests/cases/conformance/types/union/discriminatedUnionTypes2.ts(132,11): error T
143142
x.value; // number
144143
}
145144
else {
146-
x.value; // Error, x is never
147-
~~~~~
148-
!!! error TS2339: Property 'value' does not exist on type 'never'.
145+
x.value; // number
149146
}
150147
}
151148

tests/baselines/reference/discriminatedUnionTypes2.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function foo1(x: RuntimeValue & { type: 'number' }) {
130130
x.value; // number
131131
}
132132
else {
133-
x.value; // Error, x is never
133+
x.value; // number
134134
}
135135
}
136136

@@ -226,7 +226,7 @@ function foo1(x) {
226226
x.value; // number
227227
}
228228
else {
229-
x.value; // Error, x is never
229+
x.value; // number
230230
}
231231
}
232232
function foo2(x) {

tests/baselines/reference/discriminatedUnionTypes2.symbols

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,9 @@ function f(problem: abc & (b | c)) {
317317
>c : Symbol(c, Decl(discriminatedUnionTypes2.ts, 104, 1))
318318

319319
if (problem.type === 'b') {
320-
>problem.type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 101, 10), Decl(discriminatedUnionTypes2.ts, 105, 10), Decl(discriminatedUnionTypes2.ts, 97, 10), Decl(discriminatedUnionTypes2.ts, 101, 10), Decl(discriminatedUnionTypes2.ts, 97, 10) ... and 5 more)
320+
>problem.type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 101, 10), Decl(discriminatedUnionTypes2.ts, 105, 10))
321321
>problem : Symbol(problem, Decl(discriminatedUnionTypes2.ts, 112, 11))
322-
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 101, 10), Decl(discriminatedUnionTypes2.ts, 105, 10), Decl(discriminatedUnionTypes2.ts, 97, 10), Decl(discriminatedUnionTypes2.ts, 101, 10), Decl(discriminatedUnionTypes2.ts, 97, 10) ... and 5 more)
322+
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 101, 10), Decl(discriminatedUnionTypes2.ts, 105, 10))
323323

324324
problem.name;
325325
>problem.name : Symbol(name, Decl(discriminatedUnionTypes2.ts, 102, 14))
@@ -356,18 +356,20 @@ function foo1(x: RuntimeValue & { type: 'number' }) {
356356
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 126, 33))
357357

358358
if (x.type === 'number') {
359-
>x.type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 126, 33), Decl(discriminatedUnionTypes2.ts, 123, 7), Decl(discriminatedUnionTypes2.ts, 126, 33), Decl(discriminatedUnionTypes2.ts, 124, 7) ... and 1 more)
359+
>x.type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 126, 33))
360360
>x : Symbol(x, Decl(discriminatedUnionTypes2.ts, 126, 14))
361-
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 126, 33), Decl(discriminatedUnionTypes2.ts, 123, 7), Decl(discriminatedUnionTypes2.ts, 126, 33), Decl(discriminatedUnionTypes2.ts, 124, 7) ... and 1 more)
361+
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 126, 33))
362362

363363
x.value; // number
364364
>x.value : Symbol(value, Decl(discriminatedUnionTypes2.ts, 122, 23))
365365
>x : Symbol(x, Decl(discriminatedUnionTypes2.ts, 126, 14))
366366
>value : Symbol(value, Decl(discriminatedUnionTypes2.ts, 122, 23))
367367
}
368368
else {
369-
x.value; // Error, x is never
369+
x.value; // number
370+
>x.value : Symbol(value, Decl(discriminatedUnionTypes2.ts, 122, 23))
370371
>x : Symbol(x, Decl(discriminatedUnionTypes2.ts, 126, 14))
372+
>value : Symbol(value, Decl(discriminatedUnionTypes2.ts, 122, 23))
371373
}
372374
}
373375

@@ -379,9 +381,9 @@ function foo2(x: RuntimeValue & ({ type: 'number' } | { type: 'string' })) {
379381
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 135, 55))
380382

381383
if (x.type === 'number') {
382-
>x.type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 135, 34), Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 135, 55), Decl(discriminatedUnionTypes2.ts, 123, 7) ... and 7 more)
384+
>x.type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 135, 34), Decl(discriminatedUnionTypes2.ts, 123, 7), Decl(discriminatedUnionTypes2.ts, 135, 55))
383385
>x : Symbol(x, Decl(discriminatedUnionTypes2.ts, 135, 14))
384-
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 135, 34), Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 135, 55), Decl(discriminatedUnionTypes2.ts, 123, 7) ... and 7 more)
386+
>type : Symbol(type, Decl(discriminatedUnionTypes2.ts, 122, 7), Decl(discriminatedUnionTypes2.ts, 135, 34), Decl(discriminatedUnionTypes2.ts, 123, 7), Decl(discriminatedUnionTypes2.ts, 135, 55))
385387

386388
x.value; // number
387389
>x.value : Symbol(value, Decl(discriminatedUnionTypes2.ts, 122, 23))

tests/baselines/reference/discriminatedUnionTypes2.types

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -323,13 +323,13 @@ type abc = a | b | c;
323323
>abc : abc
324324

325325
function f(problem: abc & (b | c)) {
326-
>f : (problem: b | c | (a & b) | (a & c) | (b & c) | (c & b)) => void
327-
>problem : b | c | (a & b) | (a & c) | (b & c) | (c & b)
326+
>f : (problem: b | c) => void
327+
>problem : b | c
328328

329329
if (problem.type === 'b') {
330330
>problem.type === 'b' : boolean
331331
>problem.type : "b" | "c"
332-
>problem : b | c | (a & b) | (a & c) | (b & c) | (c & b)
332+
>problem : b | c
333333
>type : "b" | "c"
334334
>'b' : "b"
335335

@@ -362,14 +362,14 @@ type RuntimeValue =
362362
>value : boolean
363363

364364
function foo1(x: RuntimeValue & { type: 'number' }) {
365-
>foo1 : (x: ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "number"; }) | ({ type: "boolean"; value: boolean; } & { type: "number"; })) => void
366-
>x : ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "number"; }) | ({ type: "boolean"; value: boolean; } & { type: "number"; })
365+
>foo1 : (x: { type: "number"; value: number; } & { type: "number"; }) => void
366+
>x : { type: "number"; value: number; } & { type: "number"; }
367367
>type : "number"
368368

369369
if (x.type === 'number') {
370370
>x.type === 'number' : boolean
371371
>x.type : "number"
372-
>x : ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "number"; }) | ({ type: "boolean"; value: boolean; } & { type: "number"; })
372+
>x : { type: "number"; value: number; } & { type: "number"; }
373373
>type : "number"
374374
>'number' : "number"
375375

@@ -379,23 +379,23 @@ function foo1(x: RuntimeValue & { type: 'number' }) {
379379
>value : number
380380
}
381381
else {
382-
x.value; // Error, x is never
383-
>x.value : any
384-
>x : never
385-
>value : any
382+
x.value; // number
383+
>x.value : number
384+
>x : { type: "number"; value: number; } & { type: "number"; }
385+
>value : number
386386
}
387387
}
388388

389389
function foo2(x: RuntimeValue & ({ type: 'number' } | { type: 'string' })) {
390-
>foo2 : (x: ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "number"; value: number; } & { type: "string"; }) | ({ type: "string"; value: string; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "string"; }) | ({ type: "boolean"; value: boolean; } & { type: "number"; }) | ({ type: "boolean"; value: boolean; } & { type: "string"; })) => void
391-
>x : ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "number"; value: number; } & { type: "string"; }) | ({ type: "string"; value: string; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "string"; }) | ({ type: "boolean"; value: boolean; } & { type: "number"; }) | ({ type: "boolean"; value: boolean; } & { type: "string"; })
390+
>foo2 : (x: ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "string"; })) => void
391+
>x : ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "string"; })
392392
>type : "number"
393393
>type : "string"
394394

395395
if (x.type === 'number') {
396396
>x.type === 'number' : boolean
397397
>x.type : "string" | "number"
398-
>x : ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "number"; value: number; } & { type: "string"; }) | ({ type: "string"; value: string; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "string"; }) | ({ type: "boolean"; value: boolean; } & { type: "number"; }) | ({ type: "boolean"; value: boolean; } & { type: "string"; })
398+
>x : ({ type: "number"; value: number; } & { type: "number"; }) | ({ type: "string"; value: string; } & { type: "string"; })
399399
>type : "string" | "number"
400400
>'number' : "number"
401401

tests/baselines/reference/intersectionReduction.errors.txt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
tests/cases/conformance/types/intersection/intersectionReduction.ts(40,1): error TS2322: Type 'any' is not assignable to type 'never'.
2-
tests/cases/conformance/types/intersection/intersectionReduction.ts(41,1): error TS2322: Type 'any' is not assignable to type 'never'.
1+
tests/cases/conformance/types/intersection/intersectionReduction.ts(38,4): error TS2339: Property 'kind' does not exist on type 'never'.
2+
tests/cases/conformance/types/intersection/intersectionReduction.ts(80,1): error TS2322: Type 'any' is not assignable to type 'never'.
3+
tests/cases/conformance/types/intersection/intersectionReduction.ts(81,1): error TS2322: Type 'any' is not assignable to type 'never'.
34

45

5-
==== tests/cases/conformance/types/intersection/intersectionReduction.ts (2 errors) ====
6+
==== tests/cases/conformance/types/intersection/intersectionReduction.ts (3 errors) ====
67
declare const sym1: unique symbol;
78
declare const sym2: unique symbol;
89

@@ -35,6 +36,48 @@ tests/cases/conformance/types/intersection/intersectionReduction.ts(41,1): error
3536
type X6 = X | symbol & string;
3637
type X7 = X | void & string;
3738

39+
type A = { kind: 'a', foo: string };
40+
type B = { kind: 'b', foo: number };
41+
type C = { kind: 'c', foo: number };
42+
43+
declare let ab: A & B;
44+
ab.kind; // Error
45+
~~~~
46+
!!! error TS2339: Property 'kind' does not exist on type 'never'.
47+
48+
declare let x: A | (B & C); // A
49+
let a: A = x;
50+
51+
type AB = A & B; // never
52+
type BC = B & C; // never
53+
54+
type U1 = Partial<A & B>; // never
55+
type U2 = Readonly<A & B>; // never
56+
type U3 = (A & B)['kind']; // never
57+
type U4 = A & B | B & C; // never
58+
type U5 = A | B & C; // A
59+
60+
type K1 = keyof (A & B); // string | number | symbol
61+
type K2 = keyof A | keyof B; // 'kind' | 'foo'
62+
63+
type Merge1<T, U> = { [P in keyof (T & U)]: P extends keyof T ? T[P] : U[P & keyof U] }
64+
type Merge2<T, U> = { [P in keyof T | keyof U]: P extends keyof T ? T[P] : U[P & keyof U] }
65+
66+
type M1 = { a: 1, b: 2 } & { a: 2, c: 3 }; // never
67+
type M2 = Merge1<{ a: 1, b: 2 }, { a: 2, c: 3 }>; // {}
68+
type M3 = Merge2<{ a: 1, b: 2 }, { a: 2, c: 3 }>; // { a: 1, b: 2, c: 3 }
69+
70+
type D = { kind: 'd', foo: unknown };
71+
type E = { kind: 'e', foo: unknown };
72+
73+
declare function f10<T>(x: { foo: T }): T;
74+
75+
declare let a1: A | D;
76+
declare let a2: A | D & E;
77+
78+
let r1 = f10(a1); // unknown
79+
let r2 = f10(a2); // string
80+
3881
// Repro from #31663
3982

4083
const x1 = { a: 'foo', b: 42 };
@@ -63,4 +106,16 @@ tests/cases/conformance/types/intersection/intersectionReduction.ts(41,1): error
63106

64107
t1 = t2;
65108
t2 = t1;
109+
110+
// Repro from #36736
111+
112+
const f1 = (t: "a" | ("b" & "c")): "a" => t;
113+
114+
type Container<Type extends string> = {
115+
type: Type;
116+
}
117+
118+
const f2 = (t: Container<"a"> | (Container<"b"> & Container<"c">)): Container<"a"> => t;
119+
const f3 = (t: Container<"a"> | (Container<"b"> & { dataB: boolean } & Container<"a">)): Container<"a"> => t;
120+
const f4 = (t: number | (Container<"b"> & { dataB: boolean } & Container<"a">)): number => t;
66121

0 commit comments

Comments
 (0)