Skip to content

Reuse "getBestMatchingType" logic during elaboration to allow for more specific elaborations #35278

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
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
203 changes: 109 additions & 94 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13622,6 +13622,19 @@ namespace ts {
return false;
}

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);
if (best) {
return getIndexedAccessTypeOrUndefined(best, nameType);
}
}
}

type ElaborationIterator = IterableIterator<{ errorNode: Node, innerExpression: Expression | undefined, nameType: Type, errorMessage?: DiagnosticMessage | undefined }>;
/**
* For every element returned from the iterator, checks that element to issue an error on a property of that element's type
Expand All @@ -13640,7 +13653,7 @@ namespace ts {
let reportedError = false;
for (let status = iterator.next(); !status.done; status = iterator.next()) {
const { errorNode: prop, innerExpression: next, nameType, errorMessage } = status.value;
const targetPropType = getIndexedAccessTypeOrUndefined(target, nameType);
const targetPropType = getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType);
if (!targetPropType || targetPropType.flags & TypeFlags.IndexedAccess) continue; // Don't elaborate on indexes on generic variables
const sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType);
if (sourcePropType && !checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) {
Expand Down Expand Up @@ -14771,7 +14784,7 @@ namespace ts {
let reducedTarget = target;
let checkTypes: Type[] | undefined;
if (target.flags & TypeFlags.Union) {
reducedTarget = findMatchingDiscriminantType(source, <UnionType>target) || filterPrimitivesIfContainsNonPrimitive(<UnionType>target);
reducedTarget = findMatchingDiscriminantType(source, <UnionType>target, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(<UnionType>target);
checkTypes = reducedTarget.flags & TypeFlags.Union ? (<UnionType>reducedTarget).types : [reducedTarget];
}
for (const prop of getPropertiesOfType(source)) {
Expand Down Expand Up @@ -14863,103 +14876,12 @@ namespace ts {
}
}
if (reportErrors) {
const bestMatchingType =
findMatchingDiscriminantType(source, target) ||
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
findBestTypeForObjectLiteral(source, target) ||
findBestTypeForInvokable(source, target) ||
findMostOverlappyType(source, target);

const bestMatchingType = getBestMatchingType(source, target, isRelatedTo);
isRelatedTo(source, bestMatchingType || targetTypes[targetTypes.length - 1], /*reportErrors*/ true);
}
return Ternary.False;
}

function findMatchingTypeReferenceOrTypeAliasReference(source: Type, unionTarget: UnionOrIntersectionType) {
const sourceObjectFlags = getObjectFlags(source);
if (sourceObjectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous) && unionTarget.flags & TypeFlags.Union) {
return find(unionTarget.types, target => {
if (target.flags & TypeFlags.Object) {
const overlapObjFlags = sourceObjectFlags & getObjectFlags(target);
if (overlapObjFlags & ObjectFlags.Reference) {
return (source as TypeReference).target === (target as TypeReference).target;
}
if (overlapObjFlags & ObjectFlags.Anonymous) {
return !!(source as AnonymousType).aliasSymbol && (source as AnonymousType).aliasSymbol === (target as AnonymousType).aliasSymbol;
}
}
return false;
});
}
}

function findBestTypeForObjectLiteral(source: Type, unionTarget: UnionOrIntersectionType) {
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && forEachType(unionTarget, isArrayLikeType)) {
return find(unionTarget.types, t => !isArrayLikeType(t));
}
}

function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) {
let signatureKind = SignatureKind.Call;
const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 ||
(signatureKind = SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0);
if (hasSignatures) {
return find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0);
}
}

function findMostOverlappyType(source: Type, unionTarget: UnionOrIntersectionType) {
let bestMatch: Type | undefined;
let matchingCount = 0;
for (const target of unionTarget.types) {
const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]);
if (overlap.flags & TypeFlags.Index) {
// perfect overlap of keys
bestMatch = target;
matchingCount = Infinity;
}
else if (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 = length(filter((overlap as UnionType).types, isUnitType));
if (len >= matchingCount) {
bestMatch = target;
matchingCount = len;
}
}
else if (isUnitType(overlap) && 1 >= matchingCount) {
bestMatch = target;
matchingCount = 1;
}
}
return bestMatch;
}

function filterPrimitivesIfContainsNonPrimitive(type: UnionType) {
if (maybeTypeOfKind(type, TypeFlags.NonPrimitive)) {
const result = filterType(type, t => !(t.flags & TypeFlags.Primitive));
if (!(result.flags & TypeFlags.Never)) {
return result;
}
}
return type;
}

// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
function findMatchingDiscriminantType(source: Type, target: Type) {
if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) {
const sourceProperties = getPropertiesOfType(source);
if (sourceProperties) {
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
if (sourcePropertiesFiltered) {
return discriminateTypeByDiscriminableItems(<UnionType>target, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo);
}
}
}
return undefined;
}

function typeRelatedToEachType(source: Type, target: IntersectionType, reportErrors: boolean): Ternary {
let result = Ternary.True;
const targetTypes = target.types;
Expand Down Expand Up @@ -16106,6 +16028,14 @@ namespace ts {
}
}

function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
return findMatchingDiscriminantType(source, target, isRelatedTo) ||
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
findBestTypeForObjectLiteral(source, target) ||
findBestTypeForInvokable(source, target) ||
findMostOverlappyType(source, target);
}

function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary): Type | undefined;
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue: Type): Type;
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: Type) {
Expand Down Expand Up @@ -36176,6 +36106,91 @@ namespace ts {
}
return false;
}

function findMatchingTypeReferenceOrTypeAliasReference(source: Type, unionTarget: UnionOrIntersectionType) {
const sourceObjectFlags = getObjectFlags(source);
if (sourceObjectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous) && unionTarget.flags & TypeFlags.Union) {
return find(unionTarget.types, target => {
if (target.flags & TypeFlags.Object) {
const overlapObjFlags = sourceObjectFlags & getObjectFlags(target);
if (overlapObjFlags & ObjectFlags.Reference) {
return (source as TypeReference).target === (target as TypeReference).target;
}
if (overlapObjFlags & ObjectFlags.Anonymous) {
return !!(source as AnonymousType).aliasSymbol && (source as AnonymousType).aliasSymbol === (target as AnonymousType).aliasSymbol;
}
}
return false;
});
}
}

function findBestTypeForObjectLiteral(source: Type, unionTarget: UnionOrIntersectionType) {
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && forEachType(unionTarget, isArrayLikeType)) {
return find(unionTarget.types, t => !isArrayLikeType(t));
}
}

function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) {
let signatureKind = SignatureKind.Call;
const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 ||
(signatureKind = SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0);
if (hasSignatures) {
return find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0);
}
}

function findMostOverlappyType(source: Type, unionTarget: UnionOrIntersectionType) {
let bestMatch: Type | undefined;
let matchingCount = 0;
for (const target of unionTarget.types) {
const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]);
if (overlap.flags & TypeFlags.Index) {
// perfect overlap of keys
bestMatch = target;
matchingCount = Infinity;
}
else if (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 = length(filter((overlap as UnionType).types, isUnitType));
if (len >= matchingCount) {
bestMatch = target;
matchingCount = len;
}
}
else if (isUnitType(overlap) && 1 >= matchingCount) {
bestMatch = target;
matchingCount = 1;
}
}
return bestMatch;
}

function filterPrimitivesIfContainsNonPrimitive(type: UnionType) {
if (maybeTypeOfKind(type, TypeFlags.NonPrimitive)) {
const result = filterType(type, t => !(t.flags & TypeFlags.Primitive));
if (!(result.flags & TypeFlags.Never)) {
return result;
}
}
return type;
}

// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
function findMatchingDiscriminantType(source: Type, target: Type, isRelatedTo: (source: Type, target: Type) => Ternary) {
if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) {
const sourceProperties = getPropertiesOfType(source);
if (sourceProperties) {
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
if (sourcePropertiesFiltered) {
return discriminateTypeByDiscriminableItems(<UnionType>target, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo);
}
}
}
return undefined;
}
}

function isNotAccessor(declaration: Declaration): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(21,33): error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34): error TS2345: Argument of type '{ icon: { props: { INVALID_PROP_NAME: string; ariaLabel: string; }; }; }' is not assignable to parameter of type '(TestProps & { children?: number; }) | ({ props2: { x: number; }; } & { children?: number; })'.
The types of 'icon.props' are incompatible between these types.
Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34): error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.


==== tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts (2 errors) ====
Expand Down Expand Up @@ -39,8 +37,7 @@ tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34

TestComponent2({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2345: Argument of type '{ icon: { props: { INVALID_PROP_NAME: string; ariaLabel: string; }; }; }' is not assignable to parameter of type '(TestProps & { children?: number; }) | ({ props2: { x: number; }; } & { children?: number; })'.
!!! error TS2345: The types of 'icon.props' are incompatible between these types.
!!! error TS2345: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
!!! error TS2345: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
!!! error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
!!! error TS2322: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
!!! related TS6500 tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts:14:3: The expected type comes from property 'props' which is declared here on type 'NestedProp<ITestProps>'

Loading