Skip to content

Commit 291ab63

Browse files
weswighamTypeScript Bot
andauthored
Reuse "getBestMatchingType" logic during elaboration to allow for more specific elaborations (#35278)
* Filter target union during elaboration to allow for more specific elaborations * Reuse best match logic instead * Update user baselines (#48) Co-authored-by: TypeScript Bot <[email protected]>
1 parent bb306a7 commit 291ab63

20 files changed

+830
-199
lines changed

src/compiler/checker.ts

Lines changed: 109 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -13791,6 +13791,19 @@ namespace ts {
1379113791
return false;
1379213792
}
1379313793

13794+
function getBestMatchIndexedAccessTypeOrUndefined(source: Type, target: Type, nameType: Type) {
13795+
const idx = getIndexedAccessTypeOrUndefined(target, nameType);
13796+
if (idx) {
13797+
return idx;
13798+
}
13799+
if (target.flags & TypeFlags.Union) {
13800+
const best = getBestMatchingType(source, target as UnionType);
13801+
if (best) {
13802+
return getIndexedAccessTypeOrUndefined(best, nameType);
13803+
}
13804+
}
13805+
}
13806+
1379413807
type ElaborationIterator = IterableIterator<{ errorNode: Node, innerExpression: Expression | undefined, nameType: Type, errorMessage?: DiagnosticMessage | undefined }>;
1379513808
/**
1379613809
* For every element returned from the iterator, checks that element to issue an error on a property of that element's type
@@ -13809,7 +13822,7 @@ namespace ts {
1380913822
let reportedError = false;
1381013823
for (let status = iterator.next(); !status.done; status = iterator.next()) {
1381113824
const { errorNode: prop, innerExpression: next, nameType, errorMessage } = status.value;
13812-
const targetPropType = getIndexedAccessTypeOrUndefined(target, nameType);
13825+
const targetPropType = getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType);
1381313826
if (!targetPropType || targetPropType.flags & TypeFlags.IndexedAccess) continue; // Don't elaborate on indexes on generic variables
1381413827
const sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType);
1381513828
if (sourcePropType && !checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) {
@@ -14942,7 +14955,7 @@ namespace ts {
1494214955
let reducedTarget = target;
1494314956
let checkTypes: Type[] | undefined;
1494414957
if (target.flags & TypeFlags.Union) {
14945-
reducedTarget = findMatchingDiscriminantType(source, <UnionType>target) || filterPrimitivesIfContainsNonPrimitive(<UnionType>target);
14958+
reducedTarget = findMatchingDiscriminantType(source, <UnionType>target, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(<UnionType>target);
1494614959
checkTypes = reducedTarget.flags & TypeFlags.Union ? (<UnionType>reducedTarget).types : [reducedTarget];
1494714960
}
1494814961
for (const prop of getPropertiesOfType(source)) {
@@ -15034,103 +15047,12 @@ namespace ts {
1503415047
}
1503515048
}
1503615049
if (reportErrors) {
15037-
const bestMatchingType =
15038-
findMatchingDiscriminantType(source, target) ||
15039-
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
15040-
findBestTypeForObjectLiteral(source, target) ||
15041-
findBestTypeForInvokable(source, target) ||
15042-
findMostOverlappyType(source, target);
15043-
15050+
const bestMatchingType = getBestMatchingType(source, target, isRelatedTo);
1504415051
isRelatedTo(source, bestMatchingType || targetTypes[targetTypes.length - 1], /*reportErrors*/ true);
1504515052
}
1504615053
return Ternary.False;
1504715054
}
1504815055

15049-
function findMatchingTypeReferenceOrTypeAliasReference(source: Type, unionTarget: UnionOrIntersectionType) {
15050-
const sourceObjectFlags = getObjectFlags(source);
15051-
if (sourceObjectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous) && unionTarget.flags & TypeFlags.Union) {
15052-
return find(unionTarget.types, target => {
15053-
if (target.flags & TypeFlags.Object) {
15054-
const overlapObjFlags = sourceObjectFlags & getObjectFlags(target);
15055-
if (overlapObjFlags & ObjectFlags.Reference) {
15056-
return (source as TypeReference).target === (target as TypeReference).target;
15057-
}
15058-
if (overlapObjFlags & ObjectFlags.Anonymous) {
15059-
return !!(source as AnonymousType).aliasSymbol && (source as AnonymousType).aliasSymbol === (target as AnonymousType).aliasSymbol;
15060-
}
15061-
}
15062-
return false;
15063-
});
15064-
}
15065-
}
15066-
15067-
function findBestTypeForObjectLiteral(source: Type, unionTarget: UnionOrIntersectionType) {
15068-
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && forEachType(unionTarget, isArrayLikeType)) {
15069-
return find(unionTarget.types, t => !isArrayLikeType(t));
15070-
}
15071-
}
15072-
15073-
function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) {
15074-
let signatureKind = SignatureKind.Call;
15075-
const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 ||
15076-
(signatureKind = SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0);
15077-
if (hasSignatures) {
15078-
return find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0);
15079-
}
15080-
}
15081-
15082-
function findMostOverlappyType(source: Type, unionTarget: UnionOrIntersectionType) {
15083-
let bestMatch: Type | undefined;
15084-
let matchingCount = 0;
15085-
for (const target of unionTarget.types) {
15086-
const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]);
15087-
if (overlap.flags & TypeFlags.Index) {
15088-
// perfect overlap of keys
15089-
bestMatch = target;
15090-
matchingCount = Infinity;
15091-
}
15092-
else if (overlap.flags & TypeFlags.Union) {
15093-
// We only want to account for literal types otherwise.
15094-
// If we have a union of index types, it seems likely that we
15095-
// needed to elaborate between two generic mapped types anyway.
15096-
const len = length(filter((overlap as UnionType).types, isUnitType));
15097-
if (len >= matchingCount) {
15098-
bestMatch = target;
15099-
matchingCount = len;
15100-
}
15101-
}
15102-
else if (isUnitType(overlap) && 1 >= matchingCount) {
15103-
bestMatch = target;
15104-
matchingCount = 1;
15105-
}
15106-
}
15107-
return bestMatch;
15108-
}
15109-
15110-
function filterPrimitivesIfContainsNonPrimitive(type: UnionType) {
15111-
if (maybeTypeOfKind(type, TypeFlags.NonPrimitive)) {
15112-
const result = filterType(type, t => !(t.flags & TypeFlags.Primitive));
15113-
if (!(result.flags & TypeFlags.Never)) {
15114-
return result;
15115-
}
15116-
}
15117-
return type;
15118-
}
15119-
15120-
// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
15121-
function findMatchingDiscriminantType(source: Type, target: Type) {
15122-
if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) {
15123-
const sourceProperties = getPropertiesOfType(source);
15124-
if (sourceProperties) {
15125-
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
15126-
if (sourcePropertiesFiltered) {
15127-
return discriminateTypeByDiscriminableItems(<UnionType>target, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo);
15128-
}
15129-
}
15130-
}
15131-
return undefined;
15132-
}
15133-
1513415056
function typeRelatedToEachType(source: Type, target: IntersectionType, reportErrors: boolean): Ternary {
1513515057
let result = Ternary.True;
1513615058
const targetTypes = target.types;
@@ -16303,6 +16225,14 @@ namespace ts {
1630316225
}
1630416226
}
1630516227

16228+
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
16229+
return findMatchingDiscriminantType(source, target, isRelatedTo) ||
16230+
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
16231+
findBestTypeForObjectLiteral(source, target) ||
16232+
findBestTypeForInvokable(source, target) ||
16233+
findMostOverlappyType(source, target);
16234+
}
16235+
1630616236
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary): Type | undefined;
1630716237
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue: Type): Type;
1630816238
function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: Type) {
@@ -36663,6 +36593,91 @@ namespace ts {
3666336593
}
3666436594
return false;
3666536595
}
36596+
36597+
function findMatchingTypeReferenceOrTypeAliasReference(source: Type, unionTarget: UnionOrIntersectionType) {
36598+
const sourceObjectFlags = getObjectFlags(source);
36599+
if (sourceObjectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous) && unionTarget.flags & TypeFlags.Union) {
36600+
return find(unionTarget.types, target => {
36601+
if (target.flags & TypeFlags.Object) {
36602+
const overlapObjFlags = sourceObjectFlags & getObjectFlags(target);
36603+
if (overlapObjFlags & ObjectFlags.Reference) {
36604+
return (source as TypeReference).target === (target as TypeReference).target;
36605+
}
36606+
if (overlapObjFlags & ObjectFlags.Anonymous) {
36607+
return !!(source as AnonymousType).aliasSymbol && (source as AnonymousType).aliasSymbol === (target as AnonymousType).aliasSymbol;
36608+
}
36609+
}
36610+
return false;
36611+
});
36612+
}
36613+
}
36614+
36615+
function findBestTypeForObjectLiteral(source: Type, unionTarget: UnionOrIntersectionType) {
36616+
if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && forEachType(unionTarget, isArrayLikeType)) {
36617+
return find(unionTarget.types, t => !isArrayLikeType(t));
36618+
}
36619+
}
36620+
36621+
function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) {
36622+
let signatureKind = SignatureKind.Call;
36623+
const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 ||
36624+
(signatureKind = SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0);
36625+
if (hasSignatures) {
36626+
return find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0);
36627+
}
36628+
}
36629+
36630+
function findMostOverlappyType(source: Type, unionTarget: UnionOrIntersectionType) {
36631+
let bestMatch: Type | undefined;
36632+
let matchingCount = 0;
36633+
for (const target of unionTarget.types) {
36634+
const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]);
36635+
if (overlap.flags & TypeFlags.Index) {
36636+
// perfect overlap of keys
36637+
bestMatch = target;
36638+
matchingCount = Infinity;
36639+
}
36640+
else if (overlap.flags & TypeFlags.Union) {
36641+
// We only want to account for literal types otherwise.
36642+
// If we have a union of index types, it seems likely that we
36643+
// needed to elaborate between two generic mapped types anyway.
36644+
const len = length(filter((overlap as UnionType).types, isUnitType));
36645+
if (len >= matchingCount) {
36646+
bestMatch = target;
36647+
matchingCount = len;
36648+
}
36649+
}
36650+
else if (isUnitType(overlap) && 1 >= matchingCount) {
36651+
bestMatch = target;
36652+
matchingCount = 1;
36653+
}
36654+
}
36655+
return bestMatch;
36656+
}
36657+
36658+
function filterPrimitivesIfContainsNonPrimitive(type: UnionType) {
36659+
if (maybeTypeOfKind(type, TypeFlags.NonPrimitive)) {
36660+
const result = filterType(type, t => !(t.flags & TypeFlags.Primitive));
36661+
if (!(result.flags & TypeFlags.Never)) {
36662+
return result;
36663+
}
36664+
}
36665+
return type;
36666+
}
36667+
36668+
// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
36669+
function findMatchingDiscriminantType(source: Type, target: Type, isRelatedTo: (source: Type, target: Type) => Ternary) {
36670+
if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) {
36671+
const sourceProperties = getPropertiesOfType(source);
36672+
if (sourceProperties) {
36673+
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
36674+
if (sourcePropertiesFiltered) {
36675+
return discriminateTypeByDiscriminableItems(<UnionType>target, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo);
36676+
}
36677+
}
36678+
}
36679+
return undefined;
36680+
}
3666636681
}
3666736682

3666836683
function isNotAccessor(declaration: Declaration): boolean {

tests/baselines/reference/deepExcessPropertyCheckingWhenTargetIsIntersection.errors.txt

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(21,33): error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
22
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
3-
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; })'.
4-
The types of 'icon.props' are incompatible between these types.
5-
Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
6-
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
3+
tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34): error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
4+
Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
75

86

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

4038
TestComponent2({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
4139
~~~~~~~~~~~~~~~~~~~~~~~~~~
42-
!!! 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; })'.
43-
!!! error TS2345: The types of 'icon.props' are incompatible between these types.
44-
!!! error TS2345: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
45-
!!! error TS2345: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
40+
!!! error TS2322: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
41+
!!! error TS2322: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
42+
!!! related TS6500 tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts:14:3: The expected type comes from property 'props' which is declared here on type 'NestedProp<ITestProps>'
4643

0 commit comments

Comments
 (0)