Skip to content

Improve best type matching for elementwise elaborations #57537

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
56 changes: 35 additions & 21 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import {
ConstructorTypeNode,
ConstructSignatureDeclaration,
contains,
containsNonPublicProperties,
containsParseError,
ContextFlags,
copyEntries,
Expand Down Expand Up @@ -20295,16 +20296,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getBestMatchIndexedAccessTypeOrUndefined(source: Type, target: Type, nameType: Type) {
const idx = getIndexedAccessTypeOrUndefined(target, nameType);
if (idx) {
return idx;
}
if (target.flags & TypeFlags.Union) {
const best = getBestMatchingType(source, target as UnionType);
const best = getBestMatchingType(source, target as UnionType, /*matchSingleOverlappy*/ false);
if (best) {
return getIndexedAccessTypeOrUndefined(best, nameType);
}
}
const idx = getIndexedAccessTypeOrUndefined(target, nameType);
if (idx) {
return idx;
}
}

function checkExpressionForMutableLocationWithContextualType(next: Expression, sourcePropType: Type) {
Expand Down Expand Up @@ -21957,8 +21958,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
if (reportErrors) {
// Elaborate only if we can find a best matching type in the target union
const bestMatchingType = getBestMatchingType(source, target, isRelatedTo);
// Elaborate only if we can find a best matching type in the target
const bestMatchingType = getBestMatchingType(source, target, /*matchSingleOverlappy*/ true, isRelatedTo);
if (bestMatchingType) {
isRelatedTo(source, bestMatchingType, RecursionFlags.Target, /*reportErrors*/ true, /*headMessage*/ undefined, intersectionState);
}
Expand Down Expand Up @@ -23685,12 +23686,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getPropertiesOfType(type).filter(targetProp => containsMissingType(getTypeOfSymbol(targetProp)));
}

function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, matchSingleOverlappy: boolean, isRelatedTo = compareTypesAssignable) {
return findMatchingDiscriminantType(source, target, isRelatedTo) ||
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
findBestTypeForObjectLiteral(source, target) ||
findBestTypeForInvokable(source, target) ||
findMostOverlappyType(source, target);
findMostOverlappyType(source, filterTypesForObjectLiteralMatch(source, target), /*matchSingle*/ matchSingleOverlappy);
}

function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: (readonly [() => Type, __String])[], related: (source: Type, target: Type) => boolean | Ternary) {
Expand Down Expand Up @@ -51036,10 +51036,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function findBestTypeForObjectLiteral(source: Type, unionTarget: UnionOrIntersectionType) {
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && someType(unionTarget, isArrayLikeType)) {
return find(unionTarget.types, t => !isArrayLikeType(t));
function filterTypesForObjectLiteralMatch(source: Type, target: UnionOrIntersectionType) {
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && target.flags & TypeFlags.Union) {
const filtered = filterType(target, t =>
!(t.flags & TypeFlags.Primitive) &&
!isArrayLikeType(t) &&
!typeHasCallOrConstructSignatures(t) &&
(everyContainedType(t, t => !t.symbol || !(t.symbol.flags & SymbolFlags.Class)) || !containsNonPublicProperties(getAugmentedPropertiesOfType(t))));
if (!(filtered.flags & TypeFlags.Never)) {
return filtered;
}
}
return target;
}

function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) {
Expand All @@ -51051,31 +51059,37 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function findMostOverlappyType(source: Type, unionTarget: UnionOrIntersectionType) {
let bestMatch: Type | undefined;
function findMostOverlappyType(source: Type, target: Type, matchSingle: boolean) {
if (!(target.flags & TypeFlags.UnionOrIntersection)) {
return target;
}
let bestMatches: Type[] = [];
if (!(source.flags & (TypeFlags.Primitive | TypeFlags.InstantiablePrimitive))) {
let matchingCount = 0;
for (const target of unionTarget.types) {
if (!(target.flags & (TypeFlags.Primitive | TypeFlags.InstantiablePrimitive))) {
const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]);
for (const type of (target as UnionOrIntersectionType).types) {
if (!(type.flags & (TypeFlags.Primitive | TypeFlags.InstantiablePrimitive))) {
const overlap = getIntersectionType([getIndexType(source), getIndexType(type)]);
if (overlap.flags & TypeFlags.Index) {
// perfect overlap of keys
return target;
return type;
}
else if (isUnitType(overlap) || overlap.flags & TypeFlags.Union) {
// We only want to account for literal types otherwise.
// If we have a union of index types, it seems likely that we
// needed to elaborate between two generic mapped types anyway.
const len = overlap.flags & TypeFlags.Union ? countWhere((overlap as UnionType).types, isUnitType) : 1;
if (len >= matchingCount) {
bestMatch = target;
if (len > matchingCount || matchSingle) {
bestMatches.length = 0;
}
bestMatches = append(bestMatches, type);
matchingCount = len;
}
}
}
}
}
return bestMatch;
return bestMatches.length ? getUnionType(bestMatches) : undefined;
}

function filterPrimitivesIfContainsNonPrimitive(type: UnionType) {
Expand Down
7 changes: 7 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11628,3 +11628,10 @@ export function hasInferredType(node: Node): node is HasInferredType {
return false;
}
}

/** @internal */
export function containsNonPublicProperties(props: Symbol[]) {
return some(props, p =>
!!(getDeclarationModifierFlagsFromSymbol(p) & ModifierFlags.NonPublicAccessibilityModifier) ||
!!p.valueDeclaration && isNamedDeclaration(p.valueDeclaration) && isPrivateIdentifier(p.valueDeclaration.name));
}
5 changes: 1 addition & 4 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CompletionTriggerKind,
concatenate,
ConstructorDeclaration,
containsNonPublicProperties,
ContextFlags,
countWhere,
createModuleSpecifierResolutionHost,
Expand Down Expand Up @@ -5468,10 +5469,6 @@ function getApparentProperties(type: Type, node: ObjectLiteralExpression | JsxAt
|| memberType.isClass() && containsNonPublicProperties(memberType.getApparentProperties()))));
}

function containsNonPublicProperties(props: Symbol[]) {
return some(props, p => !!(getDeclarationModifierFlagsFromSymbol(p) & ModifierFlags.NonPublicAccessibilityModifier));
}

/**
* Gets all properties on a type, but if that type is a union of several types,
* excludes array-like types or callable/constructable types.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,10 @@ contextualTypeWithUnionTypeObjectLiteral.ts(20,5): error TS2322: Type '{ prop: s
Types of property 'prop' are incompatible.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
contextualTypeWithUnionTypeObjectLiteral.ts(21,5): error TS2322: Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; } | { prop: number; }'.
Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; }'.
Types of property 'prop' are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
contextualTypeWithUnionTypeObjectLiteral.ts(25,5): error TS2322: Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; } | { prop: number; anotherP1: number; }'.
Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; }'.
Types of property 'prop' are incompatible.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
contextualTypeWithUnionTypeObjectLiteral.ts(22,5): error TS2322: Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
contextualTypeWithUnionTypeObjectLiteral.ts(26,5): error TS2322: Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
contextualTypeWithUnionTypeObjectLiteral.ts(29,5): error TS2322: Type '{ prop: string | number; anotherP: string; anotherP1: number; }' is not assignable to type '{ prop: string; anotherP: string; } | { prop: number; anotherP1: number; }'.
Type '{ prop: string | number; anotherP: string; anotherP1: number; }' is not assignable to type '{ prop: number; anotherP1: number; }'.
Types of property 'prop' are incompatible.
Expand Down Expand Up @@ -63,23 +57,19 @@ contextualTypeWithUnionTypeObjectLiteral.ts(58,5): error TS2322: Type '(a: strin
!!! error TS2322: Type 'string | number' is not assignable to type 'number'.
!!! error TS2322: Type 'string' is not assignable to type 'number'.
var objStrOrNum6: { prop: string; anotherP: string; } | { prop: number } = {
~~~~~~~~~~~~
!!! error TS2322: Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; } | { prop: number; }'.
!!! error TS2322: Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; }'.
!!! error TS2322: Types of property 'prop' are incompatible.
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
prop: strOrNumber,
~~~~
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
!!! related TS6500 contextualTypeWithUnionTypeObjectLiteral.ts:21:21: The expected type comes from property 'prop' which is declared here on type '{ prop: string; anotherP: string; } | { prop: number; }'
anotherP: str
};
var objStrOrNum7: { prop: string; anotherP: string; } | { prop: number; anotherP1: number } = {
~~~~~~~~~~~~
!!! error TS2322: Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; } | { prop: number; anotherP1: number; }'.
!!! error TS2322: Type '{ prop: string | number; anotherP: string; }' is not assignable to type '{ prop: string; anotherP: string; }'.
!!! error TS2322: Types of property 'prop' are incompatible.
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
prop: strOrNumber,
~~~~
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
!!! error TS2322: Type 'number' is not assignable to type 'string'.
!!! related TS6500 contextualTypeWithUnionTypeObjectLiteral.ts:25:21: The expected type comes from property 'prop' which is declared here on type '{ prop: string; anotherP: string; } | { prop: number; anotherP1: number; }'
anotherP: str
};
var objStrOrNum8: { prop: string; anotherP: string; } | { prop: number; anotherP1: number } = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
discriminateWithMissingProperty.ts(12,5): error TS2345: Argument of type '{ mode: "numeric"; data: Uint8Array; }' is not assignable to parameter of type 'Arg'.
Types of property 'data' are incompatible.
Type 'Uint8Array' is not assignable to type 'number'.
discriminateWithMissingProperty.ts(12,24): error TS2322: Type 'Uint8Array' is not assignable to type 'number'.


==== discriminateWithMissingProperty.ts (1 errors) ====
Expand All @@ -16,7 +14,6 @@ discriminateWithMissingProperty.ts(12,5): error TS2345: Argument of type '{ mode

declare function foo(arg: Arg): void;
foo({ mode: "numeric", data: new Uint8Array([30]) }); // Should error
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2345: Argument of type '{ mode: "numeric"; data: Uint8Array; }' is not assignable to parameter of type 'Arg'.
!!! error TS2345: Types of property 'data' are incompatible.
!!! error TS2345: Type 'Uint8Array' is not assignable to type 'number'.
~~~~
!!! error TS2322: Type 'Uint8Array' is not assignable to type 'number'.
!!! related TS6500 discriminateWithMissingProperty.ts:3:5: The expected type comes from property 'data' which is declared here on type 'Arg'
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
elementWiseErrorInUnionTarget1.ts(19,5): error TS2322: Type 'number' is not assignable to type 'string'.


==== elementWiseErrorInUnionTarget1.ts (1 errors) ====
type SingleOrArray<T> = T | readonly T[];

type ProvidedActor = {
src: string;
input?: unknown;
};

type MachineConfig<TActors extends ProvidedActor> = {
invoke: SingleOrArray<TActors>;
};

declare function setup<TActors extends ProvidedActor>(): {
createMachine: (config: MachineConfig<TActors>) => void;
};

setup<{ src: "fetchUser"; input: string }>().createMachine({
invoke: {
src: "fetchUser",
input: 10,
~~~~~
!!! error TS2322: Type 'number' is not assignable to type 'string'.
},
});

64 changes: 64 additions & 0 deletions tests/baselines/reference/elementWiseErrorInUnionTarget1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//// [tests/cases/compiler/elementWiseErrorInUnionTarget1.ts] ////

=== elementWiseErrorInUnionTarget1.ts ===
type SingleOrArray<T> = T | readonly T[];
>SingleOrArray : Symbol(SingleOrArray, Decl(elementWiseErrorInUnionTarget1.ts, 0, 0))
>T : Symbol(T, Decl(elementWiseErrorInUnionTarget1.ts, 0, 19))
>T : Symbol(T, Decl(elementWiseErrorInUnionTarget1.ts, 0, 19))
>T : Symbol(T, Decl(elementWiseErrorInUnionTarget1.ts, 0, 19))

type ProvidedActor = {
>ProvidedActor : Symbol(ProvidedActor, Decl(elementWiseErrorInUnionTarget1.ts, 0, 41))

src: string;
>src : Symbol(src, Decl(elementWiseErrorInUnionTarget1.ts, 2, 22))

input?: unknown;
>input : Symbol(input, Decl(elementWiseErrorInUnionTarget1.ts, 3, 14))

};

type MachineConfig<TActors extends ProvidedActor> = {
>MachineConfig : Symbol(MachineConfig, Decl(elementWiseErrorInUnionTarget1.ts, 5, 2))
>TActors : Symbol(TActors, Decl(elementWiseErrorInUnionTarget1.ts, 7, 19))
>ProvidedActor : Symbol(ProvidedActor, Decl(elementWiseErrorInUnionTarget1.ts, 0, 41))

invoke: SingleOrArray<TActors>;
>invoke : Symbol(invoke, Decl(elementWiseErrorInUnionTarget1.ts, 7, 53))
>SingleOrArray : Symbol(SingleOrArray, Decl(elementWiseErrorInUnionTarget1.ts, 0, 0))
>TActors : Symbol(TActors, Decl(elementWiseErrorInUnionTarget1.ts, 7, 19))

};

declare function setup<TActors extends ProvidedActor>(): {
>setup : Symbol(setup, Decl(elementWiseErrorInUnionTarget1.ts, 9, 2))
>TActors : Symbol(TActors, Decl(elementWiseErrorInUnionTarget1.ts, 11, 23))
>ProvidedActor : Symbol(ProvidedActor, Decl(elementWiseErrorInUnionTarget1.ts, 0, 41))

createMachine: (config: MachineConfig<TActors>) => void;
>createMachine : Symbol(createMachine, Decl(elementWiseErrorInUnionTarget1.ts, 11, 58))
>config : Symbol(config, Decl(elementWiseErrorInUnionTarget1.ts, 12, 18))
>MachineConfig : Symbol(MachineConfig, Decl(elementWiseErrorInUnionTarget1.ts, 5, 2))
>TActors : Symbol(TActors, Decl(elementWiseErrorInUnionTarget1.ts, 11, 23))

};

setup<{ src: "fetchUser"; input: string }>().createMachine({
>setup<{ src: "fetchUser"; input: string }>().createMachine : Symbol(createMachine, Decl(elementWiseErrorInUnionTarget1.ts, 11, 58))
>setup : Symbol(setup, Decl(elementWiseErrorInUnionTarget1.ts, 9, 2))
>src : Symbol(src, Decl(elementWiseErrorInUnionTarget1.ts, 15, 7))
>input : Symbol(input, Decl(elementWiseErrorInUnionTarget1.ts, 15, 25))
>createMachine : Symbol(createMachine, Decl(elementWiseErrorInUnionTarget1.ts, 11, 58))

invoke: {
>invoke : Symbol(invoke, Decl(elementWiseErrorInUnionTarget1.ts, 15, 60))

src: "fetchUser",
>src : Symbol(src, Decl(elementWiseErrorInUnionTarget1.ts, 16, 11))

input: 10,
>input : Symbol(input, Decl(elementWiseErrorInUnionTarget1.ts, 17, 21))

},
});

Loading