Skip to content

Commit ab2729a

Browse files
authored
Smarter subtype reduction in union types (#42353)
* Exclude primitive types from union subtype reduction in most cases * Accept new baselines * Minor fixes * Less aggressive checking of assertion function calls that don't affect control flow * Accept new baselines
1 parent 258be21 commit ab2729a

15 files changed

+70
-5093
lines changed

src/compiler/checker.ts

Lines changed: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13310,54 +13310,40 @@ namespace ts {
1331013310
return includes;
1331113311
}
1331213312

13313-
function isSetOfLiteralsFromSameEnum(types: readonly Type[]): boolean {
13314-
const first = types[0];
13315-
if (first.flags & TypeFlags.EnumLiteral) {
13316-
const firstEnum = getParentOfSymbol(first.symbol);
13317-
for (let i = 1; i < types.length; i++) {
13318-
const other = types[i];
13319-
if (!(other.flags & TypeFlags.EnumLiteral) || (firstEnum !== getParentOfSymbol(other.symbol))) {
13320-
return false;
13321-
}
13322-
}
13323-
return true;
13324-
}
13325-
13326-
return false;
13327-
}
13328-
13329-
function removeSubtypes(types: Type[], primitivesOnly: boolean): boolean {
13313+
function removeSubtypes(types: Type[], hasObjectTypes: boolean): boolean {
13314+
// We assume that redundant primitive types have already been removed from the types array and that there
13315+
// are no any and unknown types in the array. Thus, the only possible supertypes for primitive types are empty
13316+
// object types, and if none of those are present we can exclude primitive types from the subtype check.
13317+
const hasEmptyObject = hasObjectTypes && some(types, t => !!(t.flags & TypeFlags.Object) && !isGenericMappedType(t) && isEmptyResolvedType(resolveStructuredTypeMembers(<ObjectType>t)));
1333013318
const len = types.length;
13331-
if (len === 0 || isSetOfLiteralsFromSameEnum(types)) {
13332-
return true;
13333-
}
1333413319
let i = len;
1333513320
let count = 0;
1333613321
while (i > 0) {
1333713322
i--;
1333813323
const source = types[i];
13339-
for (const target of types) {
13340-
if (source !== target) {
13341-
if (count === 100000) {
13342-
// After 100000 subtype checks we estimate the remaining amount of work by assuming the
13343-
// same ratio of checks per element. If the estimated number of remaining type checks is
13344-
// greater than an upper limit we deem the union type too complex to represent. The
13345-
// upper limit is 25M for unions of primitives only, and 1M otherwise. This for example
13346-
// caps union types at 5000 unique literal types and 1000 unique object types.
13347-
const estimatedCount = (count / (len - i)) * len;
13348-
if (estimatedCount > (primitivesOnly ? 25000000 : 1000000)) {
13349-
tracing.instant(tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) });
13350-
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
13351-
return false;
13324+
if (hasEmptyObject || source.flags & TypeFlags.StructuredOrInstantiable) {
13325+
for (const target of types) {
13326+
if (source !== target) {
13327+
if (count === 100000) {
13328+
// After 100000 subtype checks we estimate the remaining amount of work by assuming the
13329+
// same ratio of checks per element. If the estimated number of remaining type checks is
13330+
// greater than 1M we deem the union type too complex to represent. This for example
13331+
// caps union types at 1000 unique object types.
13332+
const estimatedCount = (count / (len - i)) * len;
13333+
if (estimatedCount > 1000000) {
13334+
tracing.instant(tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) });
13335+
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
13336+
return false;
13337+
}
13338+
}
13339+
count++;
13340+
if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (
13341+
!(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) ||
13342+
!(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) ||
13343+
isTypeDerivedFrom(source, target))) {
13344+
orderedRemoveItemAt(types, i);
13345+
break;
1335213346
}
13353-
}
13354-
count++;
13355-
if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (
13356-
!(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) ||
13357-
!(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) ||
13358-
isTypeDerivedFrom(source, target))) {
13359-
orderedRemoveItemAt(types, i);
13360-
break;
1336113347
}
1336213348
}
1336313349
}
@@ -13370,11 +13356,13 @@ namespace ts {
1337013356
while (i > 0) {
1337113357
i--;
1337213358
const t = types[i];
13359+
const flags = t.flags;
1337313360
const remove =
13374-
t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String ||
13375-
t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number ||
13376-
t.flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt ||
13377-
t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol ||
13361+
flags & TypeFlags.StringLiteral && includes & TypeFlags.String ||
13362+
flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number ||
13363+
flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt ||
13364+
flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol ||
13365+
flags & TypeFlags.Undefined && includes & TypeFlags.Void ||
1337813366
isFreshLiteralType(t) && containsType(types, (<LiteralType>t).regularType);
1337913367
if (remove) {
1338013368
orderedRemoveItemAt(types, i);
@@ -13440,20 +13428,18 @@ namespace ts {
1344013428
if (includes & TypeFlags.AnyOrUnknown) {
1344113429
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
1344213430
}
13443-
switch (unionReduction) {
13444-
case UnionReduction.Literal:
13445-
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol)) {
13446-
removeRedundantLiteralTypes(typeSet, includes);
13447-
}
13448-
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
13449-
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
13450-
}
13451-
break;
13452-
case UnionReduction.Subtype:
13453-
if (!removeSubtypes(typeSet, !(includes & TypeFlags.IncludesStructuredOrInstantiable))) {
13454-
return errorType;
13455-
}
13456-
break;
13431+
if (unionReduction & (UnionReduction.Literal | UnionReduction.Subtype)) {
13432+
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
13433+
removeRedundantLiteralTypes(typeSet, includes);
13434+
}
13435+
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
13436+
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
13437+
}
13438+
}
13439+
if (unionReduction & UnionReduction.Subtype) {
13440+
if (!removeSubtypes(typeSet, !!(includes & TypeFlags.Object))) {
13441+
return errorType;
13442+
}
1345713443
}
1345813444
if (typeSet.length === 0) {
1345913445
return includes & TypeFlags.Null ? includes & TypeFlags.IncludesNonWideningType ? nullType : nullWideningType :
@@ -28985,7 +28971,7 @@ namespace ts {
2898528971
if (returnType.flags & TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) {
2898628972
return getESSymbolLikeTypeForNode(walkUpParenthesizedExpressions(node.parent));
2898728973
}
28988-
if (node.kind === SyntaxKind.CallExpression && node.parent.kind === SyntaxKind.ExpressionStatement &&
28974+
if (node.kind === SyntaxKind.CallExpression && !node.questionDotToken && node.parent.kind === SyntaxKind.ExpressionStatement &&
2898928975
returnType.flags & TypeFlags.Void && getTypePredicateOfSignature(signature)) {
2899028976
if (!isDottedName(node.expression)) {
2899128977
error(node.expression, Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name);

tests/baselines/reference/callChain.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ declare const o5: <T>() => undefined | (() => void);
260260
>o5 : <T>() => undefined | (() => void)
261261

262262
o5<number>()?.();
263-
>o5<number>()?.() : void | undefined
263+
>o5<number>()?.() : void
264264
>o5<number>() : (() => void) | undefined
265265
>o5 : <T>() => (() => void) | undefined
266266

tests/baselines/reference/callChainInference.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ if (value) {
2929
}
3030

3131
value?.foo("a");
32-
>value?.foo("a") : void | undefined
32+
>value?.foo("a") : void
3333
>value?.foo : (<T>(this: T, arg: keyof T) => void) | undefined
3434
>value : Y | undefined
3535
>foo : (<T>(this: T, arg: keyof T) => void) | undefined

tests/baselines/reference/controlFlowOptionalChain.errors.txt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(112,1): error TS
1818
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(112,1): error TS2532: Object is possibly 'undefined'.
1919
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(130,5): error TS2532: Object is possibly 'undefined'.
2020
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(134,1): error TS2532: Object is possibly 'undefined'.
21-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
2221
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(208,9): error TS2532: Object is possibly 'undefined'.
2322
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(211,9): error TS2532: Object is possibly 'undefined'.
2423
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(214,9): error TS2532: Object is possibly 'undefined'.
@@ -62,7 +61,7 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(518,13): error T
6261
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(567,21): error TS2532: Object is possibly 'undefined'.
6362

6463

65-
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (62 errors) ====
64+
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (61 errors) ====
6665
// assignments in shortcutting chain
6766
declare const o: undefined | {
6867
[key: string]: any;
@@ -256,8 +255,6 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(567,21): error T
256255
if (!!true) {
257256
isDefined(maybeIsString);
258257
maybeIsString?.(x);
259-
~~~~~~~~~~~~~
260-
!!! error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
261258
x;
262259
}
263260
if (!!true) {

tests/baselines/reference/controlFlowOptionalChain.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ function f01(x: unknown) {
595595
>true : true
596596

597597
maybeIsString?.(x);
598-
>maybeIsString?.(x) : void | undefined
598+
>maybeIsString?.(x) : void
599599
>maybeIsString : ((value: unknown) => asserts value is string) | undefined
600600
>x : unknown
601601

tests/baselines/reference/controlFlowSuperPropertyAccess.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class C extends B {
1313
>body : () => void
1414

1515
super.m && super.m();
16-
>super.m && super.m() : void | undefined
16+
>super.m && super.m() : void
1717
>super.m : (() => void) | undefined
1818
>super : B
1919
>m : (() => void) | undefined

tests/baselines/reference/discriminantPropertyCheck.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ const u: U = {} as any;
343343
>{} : {}
344344

345345
u.a && u.b && f(u.a, u.b);
346-
>u.a && u.b && f(u.a, u.b) : void | "" | undefined
346+
>u.a && u.b && f(u.a, u.b) : void | ""
347347
>u.a && u.b : string | undefined
348348
>u.a : string | undefined
349349
>u : U
@@ -361,7 +361,7 @@ u.a && u.b && f(u.a, u.b);
361361
>b : string
362362

363363
u.b && u.a && f(u.a, u.b);
364-
>u.b && u.a && f(u.a, u.b) : void | "" | undefined
364+
>u.b && u.a && f(u.a, u.b) : void | ""
365365
>u.b && u.a : string | undefined
366366
>u.b : string | undefined
367367
>u : U

tests/baselines/reference/promiseTypeStrictNull.types

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -888,8 +888,8 @@ const p75 = p.then(() => undefined, () => null);
888888
>null : null
889889

890890
const p76 = p.then(() => undefined, () => {});
891-
>p76 : Promise<void | undefined>
892-
>p.then(() => undefined, () => {}) : Promise<void | undefined>
891+
>p76 : Promise<void>
892+
>p.then(() => undefined, () => {}) : Promise<void>
893893
>p.then : <TResult1 = boolean, TResult2 = never>(onfulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>
894894
>p : Promise<boolean>
895895
>then : <TResult1 = boolean, TResult2 = never>(onfulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>
@@ -1092,8 +1092,8 @@ const p93 = p.then(() => {}, () => x);
10921092
>x : any
10931093

10941094
const p94 = p.then(() => {}, () => undefined);
1095-
>p94 : Promise<void | undefined>
1096-
>p.then(() => {}, () => undefined) : Promise<void | undefined>
1095+
>p94 : Promise<void>
1096+
>p.then(() => {}, () => undefined) : Promise<void>
10971097
>p.then : <TResult1 = boolean, TResult2 = never>(onfulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>
10981098
>p : Promise<boolean>
10991099
>then : <TResult1 = boolean, TResult2 = never>(onfulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>

tests/baselines/reference/superMethodCall.types

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ class Derived extends Base {
1111
>Base : Base
1212

1313
method() {
14-
>method : () => void | undefined
14+
>method : () => void
1515

1616
return super.method?.();
17-
>super.method?.() : void | undefined
17+
>super.method?.() : void
1818
>super.method : (() => void) | undefined
1919
>super : Base
2020
>method : (() => void) | undefined
2121
}
2222

2323
async asyncMethod() {
24-
>asyncMethod : () => Promise<void | undefined>
24+
>asyncMethod : () => Promise<void>
2525

2626
return super.method?.();
27-
>super.method?.() : void | undefined
27+
>super.method?.() : void
2828
>super.method : (() => void) | undefined
2929
>super : Base
3030
>method : (() => void) | undefined

tests/baselines/reference/thisMethodCall.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class C {
99
>other : () => void
1010

1111
this.method?.();
12-
>this.method?.() : void | undefined
12+
>this.method?.() : void
1313
>this.method : (() => void) | undefined
1414
>this : this
1515
>method : (() => void) | undefined

tests/baselines/reference/truthinessCallExpressionCoercion2.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function test(required1: () => boolean, required2: () => boolean, b: boolean, op
6060

6161
// ok
6262
optional && console.log('optional');
63-
>optional && console.log('optional') : void | undefined
63+
>optional && console.log('optional') : void
6464
>optional : (() => boolean) | undefined
6565
>console.log('optional') : void
6666
>console.log : (...data: any[]) => void
@@ -70,7 +70,7 @@ function test(required1: () => boolean, required2: () => boolean, b: boolean, op
7070

7171
// ok
7272
1 && optional && console.log('optional');
73-
>1 && optional && console.log('optional') : void | undefined
73+
>1 && optional && console.log('optional') : void
7474
>1 && optional : (() => boolean) | undefined
7575
>1 : 1
7676
>optional : (() => boolean) | undefined
@@ -441,7 +441,7 @@ class Foo {
441441

442442
// ok
443443
1 && this.optional && console.log('optional');
444-
>1 && this.optional && console.log('optional') : void | undefined
444+
>1 && this.optional && console.log('optional') : void
445445
>1 && this.optional : (() => boolean) | undefined
446446
>1 : 1
447447
>this.optional : (() => boolean) | undefined

tests/baselines/reference/typeVariableTypeGuards.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class A<P extends Partial<Foo>> {
1616
>doSomething : () => void
1717

1818
this.props.foo && this.props.foo()
19-
>this.props.foo && this.props.foo() : void | undefined
19+
>this.props.foo && this.props.foo() : void
2020
>this.props.foo : P["foo"] | undefined
2121
>this.props : Readonly<P>
2222
>this : this

0 commit comments

Comments
 (0)