Skip to content
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

Smarter subtype reduction in union types #42353

Merged
merged 6 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -13255,54 +13255,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 @@ -13315,11 +13301,13 @@ namespace ts {
while (i > 0) {
i--;
const t = types[i];
const flags = t.flags;
const remove =
t.flags & TypeFlags.StringLikeLiteral && 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.StringLikeLiteral && 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 @@ -13385,20 +13373,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.FreshableLiteral | 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.FreshableLiteral | 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 @@ -28873,7 +28859,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 &&
Copy link
Member

Choose a reason for hiding this comment

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

Was this supposed to be part of this change set? Or part of another PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's an intended change. We have some code in checkCallExpression that attempts to issue errors when calls to assertion functions aren't actually processed as assertions (because, for example, they don't have targets that were declared with explicit type annotations). This code kicks in only with a function's return type is exactly void. It would not kick in in cases where the return type was void | undefined, but now that we reduce that to void, it does kick in.

The core issue really is that we don't want the check when the call expression is a question-dot call because the assertion isn't know to have actually happened. So that's what this fixes.

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
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