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

Improve best type matching for elementwise elaborations #57537

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
56 changes: 35 additions & 21 deletions src/compiler/checker.ts
Expand Up @@ -95,6 +95,7 @@ import {
ConstructorTypeNode,
ConstructSignatureDeclaration,
contains,
containsNonPublicProperties,
containsParseError,
ContextFlags,
copyEntries,
Expand Down Expand Up @@ -20352,16 +20353,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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

swapping the order of operations here improves some errors quite a bit in my eyes since the union discrimination happens early, for example:

const obj6: {
  prop:
    | {
        type: "foo";
        prop: string;
      }
    | {
        type: "bar";
        prop: number;
      };
} = {
  prop: {
    type: "foo",
    // current: Type 'boolean' is not assignable to type 'string | number'.
    // pr: Type 'boolean' is not assignable to type 'string'
    prop: true,
  },
};

const obj7: {
  prop:
    | {
        type: "foo";
        prop: string;
      }
    | {
        type: "bar";
        prop: number;
      };
} = {
  // current: Type '{ type: "foo"; prop: number; }' is not assignable to type '{ type: "foo"; prop: string; } | { type: "bar"; prop: number; }'.
  //  Types of property 'prop' are incompatible.
  //    Type 'number' is not assignable to type 'string'.
  prop: {
    type: "foo",
    // pr: Type 'number' is not assignable to type 'string'.
    prop: 42,
  },
};

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 @@ -21983,8 +21984,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
if (reportErrors) {
// Elaborate only if we can find a best matching type in the target union
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed this union from here because it was misleading - the containing function handles both unions and intersections. However, from what I can tell reportErrors: true is only ever passed in for unions - I can revert this if requested

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Returning a single overlappy type is required here to make the non-elementwise union-based elaboration to elaborate about the "last" element in the union, see the "recovered" errors in this commit: e9d136a

if (bestMatchingType) {
isRelatedTo(source, bestMatchingType, RecursionFlags.Target, /*reportErrors*/ true, /*headMessage*/ undefined, intersectionState);
}
Expand Down Expand Up @@ -23711,12 +23712,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 @@ -50901,10 +50901,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));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The referenced bug was a somewhat funny one because optional properties always include | undefined in their types and undefinedType is always kept at the beginning of unionType.types. So in such a situation this was always returning that undefinedType - making it impossible to create a good elementwise elaboration.

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))));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is quite related to #56183 - although completions and contextual types are orthogonal to elementwise elaborations. I only figured out that I should filter those nominal types here because I was working on that other PR in the past

if (!(filtered.flags & TypeFlags.Never)) {
return filtered;
}
}
return target;
}

function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) {
Expand All @@ -50916,31 +50924,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
Expand Up @@ -10671,3 +10671,10 @@ export function replaceFirstStar(s: string, replacement: string): string {
export function getNameFromImportAttribute(node: ImportAttribute) {
return isIdentifier(node.name) ? node.name.escapedText : escapeLeadingUnderscores(node.name.text);
}

/** @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
Expand Up @@ -34,6 +34,7 @@ import {
CompletionTriggerKind,
concatenate,
ConstructorDeclaration,
containsNonPublicProperties,
ContextFlags,
countWhere,
createModuleSpecifierResolutionHost,
Expand Down Expand Up @@ -5419,10 +5420,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
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
@@ -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'
@@ -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
@@ -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))

},
});

59 changes: 59 additions & 0 deletions tests/baselines/reference/elementWiseErrorInUnionTarget1.types
@@ -0,0 +1,59 @@
//// [tests/cases/compiler/elementWiseErrorInUnionTarget1.ts] ////

=== elementWiseErrorInUnionTarget1.ts ===
type SingleOrArray<T> = T | readonly T[];
>SingleOrArray : SingleOrArray<T>

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

src: string;
>src : string

input?: unknown;
>input : unknown

};

type MachineConfig<TActors extends ProvidedActor> = {
>MachineConfig : MachineConfig<TActors>

invoke: SingleOrArray<TActors>;
>invoke : SingleOrArray<TActors>

};

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

createMachine: (config: MachineConfig<TActors>) => void;
>createMachine : (config: MachineConfig<TActors>) => void
>config : MachineConfig<TActors>

};

setup<{ src: "fetchUser"; input: string }>().createMachine({
>setup<{ src: "fetchUser"; input: string }>().createMachine({ invoke: { src: "fetchUser", input: 10, },}) : void
>setup<{ src: "fetchUser"; input: string }>().createMachine : (config: MachineConfig<{ src: "fetchUser"; input: string; }>) => void
>setup<{ src: "fetchUser"; input: string }>() : { createMachine: (config: MachineConfig<{ src: "fetchUser"; input: string; }>) => void; }
>setup : <TActors extends ProvidedActor>() => { createMachine: (config: MachineConfig<TActors>) => void; }
>src : "fetchUser"
>input : string
>createMachine : (config: MachineConfig<{ src: "fetchUser"; input: string; }>) => void
>{ invoke: { src: "fetchUser", input: 10, },} : { invoke: { src: "fetchUser"; input: number; }; }

invoke: {
>invoke : { src: "fetchUser"; input: number; }
>{ src: "fetchUser", input: 10, } : { src: "fetchUser"; input: number; }

src: "fetchUser",
>src : "fetchUser"
>"fetchUser" : "fetchUser"

input: 10,
>input : number
>10 : 10

},
});