Skip to content

Smarter subtype reduction in union types #42353

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 6 commits into from
Feb 4, 2021
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
106 changes: 46 additions & 60 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13310,54 +13310,40 @@ namespace ts {
return includes;
}

function isSetOfLiteralsFromSameEnum(types: readonly Type[]): boolean {
const first = types[0];
if (first.flags & TypeFlags.EnumLiteral) {
const firstEnum = getParentOfSymbol(first.symbol);
for (let i = 1; i < types.length; i++) {
const other = types[i];
if (!(other.flags & TypeFlags.EnumLiteral) || (firstEnum !== getParentOfSymbol(other.symbol))) {
return false;
}
}
return true;
}

return false;
}

function removeSubtypes(types: Type[], primitivesOnly: boolean): boolean {
function removeSubtypes(types: Type[], hasObjectTypes: boolean): boolean {
// We assume that redundant primitive types have already been removed from the types array and that there
// are no any and unknown types in the array. Thus, the only possible supertypes for primitive types are empty
// object types, and if none of those are present we can exclude primitive types from the subtype check.
const hasEmptyObject = hasObjectTypes && some(types, t => !!(t.flags & TypeFlags.Object) && !isGenericMappedType(t) && isEmptyResolvedType(resolveStructuredTypeMembers(<ObjectType>t)));
const len = types.length;
if (len === 0 || isSetOfLiteralsFromSameEnum(types)) {
return true;
}
let i = len;
let count = 0;
while (i > 0) {
i--;
const source = types[i];
for (const target of types) {
if (source !== target) {
if (count === 100000) {
// After 100000 subtype checks we estimate the remaining amount of work by assuming the
// same ratio of checks per element. If the estimated number of remaining type checks is
// greater than an upper limit we deem the union type too complex to represent. The
// upper limit is 25M for unions of primitives only, and 1M otherwise. This for example
// caps union types at 5000 unique literal types and 1000 unique object types.
const estimatedCount = (count / (len - i)) * len;
if (estimatedCount > (primitivesOnly ? 25000000 : 1000000)) {
tracing.instant(tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) });
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
return false;
if (hasEmptyObject || source.flags & TypeFlags.StructuredOrInstantiable) {
for (const target of types) {
if (source !== target) {
if (count === 100000) {
// After 100000 subtype checks we estimate the remaining amount of work by assuming the
// same ratio of checks per element. If the estimated number of remaining type checks is
// greater than 1M we deem the union type too complex to represent. This for example
// caps union types at 1000 unique object types.
const estimatedCount = (count / (len - i)) * len;
if (estimatedCount > 1000000) {
tracing.instant(tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) });
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
return false;
}
}
count++;
if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (
!(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) ||
!(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) ||
isTypeDerivedFrom(source, target))) {
orderedRemoveItemAt(types, i);
break;
}
}
count++;
if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (
!(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) ||
!(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) ||
isTypeDerivedFrom(source, target))) {
orderedRemoveItemAt(types, i);
break;
}
}
}
Expand All @@ -13370,11 +13356,13 @@ namespace ts {
while (i > 0) {
i--;
const t = types[i];
const flags = t.flags;
const remove =
t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String ||
t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number ||
t.flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt ||
t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol ||
flags & TypeFlags.StringLiteral && includes & TypeFlags.String ||
flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number ||
flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt ||
flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol ||
flags & TypeFlags.Undefined && includes & TypeFlags.Void ||
isFreshLiteralType(t) && containsType(types, (<LiteralType>t).regularType);
if (remove) {
orderedRemoveItemAt(types, i);
Expand Down Expand Up @@ -13440,20 +13428,18 @@ namespace ts {
if (includes & TypeFlags.AnyOrUnknown) {
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
}
switch (unionReduction) {
case UnionReduction.Literal:
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol)) {
removeRedundantLiteralTypes(typeSet, includes);
}
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
}
break;
case UnionReduction.Subtype:
if (!removeSubtypes(typeSet, !(includes & TypeFlags.IncludesStructuredOrInstantiable))) {
return errorType;
}
break;
if (unionReduction & (UnionReduction.Literal | UnionReduction.Subtype)) {
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
removeRedundantLiteralTypes(typeSet, includes);
}
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
}
}
if (unionReduction & UnionReduction.Subtype) {
if (!removeSubtypes(typeSet, !!(includes & TypeFlags.Object))) {
return errorType;
}
}
if (typeSet.length === 0) {
return includes & TypeFlags.Null ? includes & TypeFlags.IncludesNonWideningType ? nullType : nullWideningType :
Expand Down Expand Up @@ -28985,7 +28971,7 @@ namespace ts {
if (returnType.flags & TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) {
return getESSymbolLikeTypeForNode(walkUpParenthesizedExpressions(node.parent));
}
if (node.kind === SyntaxKind.CallExpression && node.parent.kind === SyntaxKind.ExpressionStatement &&
if (node.kind === SyntaxKind.CallExpression && !node.questionDotToken && node.parent.kind === SyntaxKind.ExpressionStatement &&
returnType.flags & TypeFlags.Void && getTypePredicateOfSignature(signature)) {
if (!isDottedName(node.expression)) {
error(node.expression, Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name);
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/callChain.types
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ declare const o5: <T>() => undefined | (() => void);
>o5 : <T>() => undefined | (() => void)

o5<number>()?.();
>o5<number>()?.() : void | undefined
>o5<number>()?.() : void
>o5<number>() : (() => void) | undefined
>o5 : <T>() => (() => void) | undefined

Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/callChainInference.types
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if (value) {
}

value?.foo("a");
>value?.foo("a") : void | undefined
>value?.foo("a") : void
>value?.foo : (<T>(this: T, arg: keyof T) => void) | undefined
>value : Y | undefined
>foo : (<T>(this: T, arg: keyof T) => void) | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(112,1): error TS
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(112,1): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(130,5): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(134,1): error TS2532: Object is possibly 'undefined'.
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.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(208,9): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(211,9): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(214,9): error TS2532: Object is possibly 'undefined'.
Expand Down Expand Up @@ -62,7 +61,7 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(518,13): error T
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(567,21): error TS2532: Object is possibly 'undefined'.


==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (62 errors) ====
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (61 errors) ====
// assignments in shortcutting chain
declare const o: undefined | {
[key: string]: any;
Expand Down Expand Up @@ -256,8 +255,6 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(567,21): error T
if (!!true) {
isDefined(maybeIsString);
maybeIsString?.(x);
~~~~~~~~~~~~~
!!! error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
x;
}
if (!!true) {
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/controlFlowOptionalChain.types
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ function f01(x: unknown) {
>true : true

maybeIsString?.(x);
>maybeIsString?.(x) : void | undefined
>maybeIsString?.(x) : void
>maybeIsString : ((value: unknown) => asserts value is string) | undefined
>x : unknown

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class C extends B {
>body : () => void

super.m && super.m();
>super.m && super.m() : void | undefined
>super.m && super.m() : void
>super.m : (() => void) | undefined
>super : B
>m : (() => void) | undefined
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/discriminantPropertyCheck.types
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ const u: U = {} as any;
>{} : {}

u.a && u.b && f(u.a, u.b);
>u.a && u.b && f(u.a, u.b) : void | "" | undefined
>u.a && u.b && f(u.a, u.b) : void | ""
>u.a && u.b : string | undefined
>u.a : string | undefined
>u : U
Expand All @@ -361,7 +361,7 @@ u.a && u.b && f(u.a, u.b);
>b : string

u.b && u.a && f(u.a, u.b);
>u.b && u.a && f(u.a, u.b) : void | "" | undefined
>u.b && u.a && f(u.a, u.b) : void | ""
>u.b && u.a : string | undefined
>u.b : string | undefined
>u : U
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/promiseTypeStrictNull.types
Original file line number Diff line number Diff line change
Expand Up @@ -888,8 +888,8 @@ const p75 = p.then(() => undefined, () => null);
>null : null

const p76 = p.then(() => undefined, () => {});
>p76 : Promise<void | undefined>
>p.then(() => undefined, () => {}) : Promise<void | undefined>
>p76 : Promise<void>
>p.then(() => undefined, () => {}) : Promise<void>
>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>
>p : Promise<boolean>
>then : <TResult1 = boolean, TResult2 = never>(onfulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>
Expand Down Expand Up @@ -1092,8 +1092,8 @@ const p93 = p.then(() => {}, () => x);
>x : any

const p94 = p.then(() => {}, () => undefined);
>p94 : Promise<void | undefined>
>p.then(() => {}, () => undefined) : Promise<void | undefined>
>p94 : Promise<void>
>p.then(() => {}, () => undefined) : Promise<void>
>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>
>p : Promise<boolean>
>then : <TResult1 = boolean, TResult2 = never>(onfulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/superMethodCall.types
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ class Derived extends Base {
>Base : Base

method() {
>method : () => void | undefined
>method : () => void

return super.method?.();
>super.method?.() : void | undefined
>super.method?.() : void
>super.method : (() => void) | undefined
>super : Base
>method : (() => void) | undefined
}

async asyncMethod() {
>asyncMethod : () => Promise<void | undefined>
>asyncMethod : () => Promise<void>

return super.method?.();
>super.method?.() : void | undefined
>super.method?.() : void
>super.method : (() => void) | undefined
>super : Base
>method : (() => void) | undefined
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/thisMethodCall.types
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class C {
>other : () => void

this.method?.();
>this.method?.() : void | undefined
>this.method?.() : void
>this.method : (() => void) | undefined
>this : this
>method : (() => void) | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function test(required1: () => boolean, required2: () => boolean, b: boolean, op

// ok
optional && console.log('optional');
>optional && console.log('optional') : void | undefined
>optional && console.log('optional') : void
>optional : (() => boolean) | undefined
>console.log('optional') : void
>console.log : (...data: any[]) => void
Expand All @@ -70,7 +70,7 @@ function test(required1: () => boolean, required2: () => boolean, b: boolean, op

// ok
1 && optional && console.log('optional');
>1 && optional && console.log('optional') : void | undefined
>1 && optional && console.log('optional') : void
>1 && optional : (() => boolean) | undefined
>1 : 1
>optional : (() => boolean) | undefined
Expand Down Expand Up @@ -441,7 +441,7 @@ class Foo {

// ok
1 && this.optional && console.log('optional');
>1 && this.optional && console.log('optional') : void | undefined
>1 && this.optional && console.log('optional') : void
>1 && this.optional : (() => boolean) | undefined
>1 : 1
>this.optional : (() => boolean) | undefined
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/typeVariableTypeGuards.types
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class A<P extends Partial<Foo>> {
>doSomething : () => void

this.props.foo && this.props.foo()
>this.props.foo && this.props.foo() : void | undefined
>this.props.foo && this.props.foo() : void
>this.props.foo : P["foo"] | undefined
>this.props : Readonly<P>
>this : this
Expand Down
Loading