Skip to content

Commit 196c0aa

Browse files
authored
Fix excess property checking for unions with index signatures (#34927)
* Fix excess property checking for union targets with index signatures * Accept new baselines * Remove unused code * Add tests * Accept new baselines
1 parent 48fa3a5 commit 196c0aa

13 files changed

+299
-116
lines changed

src/compiler/checker.ts

+36-46
Original file line numberDiff line numberDiff line change
@@ -9368,25 +9368,6 @@ namespace ts {
93689368
return type.resolvedProperties;
93699369
}
93709370

9371-
function getPossiblePropertiesOfUnionType(type: UnionType): Symbol[] {
9372-
if (type.possiblePropertyCache) {
9373-
return type.possiblePropertyCache.size ? arrayFrom(type.possiblePropertyCache.values()) : emptyArray;
9374-
}
9375-
type.possiblePropertyCache = createSymbolTable();
9376-
for (const t of type.types) {
9377-
for (const p of getPropertiesOfType(t)) {
9378-
if (!type.possiblePropertyCache.has(p.escapedName)) {
9379-
const prop = getUnionOrIntersectionProperty(type, p.escapedName);
9380-
if (prop) {
9381-
type.possiblePropertyCache.set(p.escapedName, prop);
9382-
}
9383-
}
9384-
}
9385-
}
9386-
// We can't simply use the normal property cache here, since that will contain cached apparent type members :(
9387-
return type.possiblePropertyCache.size ? arrayFrom(type.possiblePropertyCache.values()) : emptyArray;
9388-
}
9389-
93909371
function getPropertiesOfType(type: Type): Symbol[] {
93919372
type = getApparentType(type);
93929373
return type.flags & TypeFlags.UnionOrIntersection ?
@@ -14614,8 +14595,7 @@ namespace ts {
1461414595
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
1461514596
const isPerformingExcessPropertyChecks = !isApparentIntersectionConstituent && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
1461614597
if (isPerformingExcessPropertyChecks) {
14617-
const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
14618-
if (hasExcessProperties(<FreshObjectLiteralType>source, target, discriminantType, reportErrors)) {
14598+
if (hasExcessProperties(<FreshObjectLiteralType>source, target, reportErrors)) {
1461914599
if (reportErrors) {
1462014600
reportRelationError(headMessage, source, target);
1462114601
}
@@ -14657,13 +14637,6 @@ namespace ts {
1465714637
else {
1465814638
if (target.flags & TypeFlags.Union) {
1465914639
result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
14660-
if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks)) {
14661-
// Validate against excess props using the original `source`
14662-
const discriminantType = findMatchingDiscriminantType(source, target as UnionType) || filterPrimitivesIfContainsNonPrimitive(target as UnionType);
14663-
if (!propertiesRelatedTo(source, discriminantType, reportErrors, /*excludedProperties*/ undefined, isIntersectionConstituent)) {
14664-
return Ternary.False;
14665-
}
14666-
}
1466714640
}
1466814641
else if (target.flags & TypeFlags.Intersection) {
1466914642
isIntersectionConstituent = true; // set here to affect the following trio of checks
@@ -14776,26 +14749,38 @@ namespace ts {
1477614749
return Ternary.False;
1477714750
}
1477814751

14779-
function hasExcessProperties(source: FreshObjectLiteralType, target: Type, discriminant: Type | undefined, reportErrors: boolean): boolean {
14780-
if (!noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) {
14752+
function getTypeOfPropertyInTypes(types: Type[], name: __String) {
14753+
const appendPropType = (propTypes: Type[] | undefined, type: Type) => {
14754+
type = getApparentType(type);
14755+
const prop = type.flags & TypeFlags.UnionOrIntersection ? getPropertyOfUnionOrIntersectionType(<UnionOrIntersectionType>type, name) : getPropertyOfObjectType(type, name);
14756+
const propType = prop && getTypeOfSymbol(prop) || isNumericLiteralName(name) && getIndexTypeOfType(type, IndexKind.Number) || getIndexTypeOfType(type, IndexKind.String) || undefinedType;
14757+
return append(propTypes, propType);
14758+
};
14759+
return getUnionType(reduceLeft(types, appendPropType, /*initial*/ undefined) || emptyArray);
14760+
}
14761+
14762+
function hasExcessProperties(source: FreshObjectLiteralType, target: Type, reportErrors: boolean): boolean {
14763+
if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) {
1478114764
return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny
1478214765
}
14783-
if (isExcessPropertyCheckTarget(target)) {
14784-
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
14785-
if ((relation === assignableRelation || relation === comparableRelation) &&
14786-
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) {
14787-
return false;
14788-
}
14789-
if (discriminant) {
14790-
// check excess properties against discriminant type only, not the entire union
14791-
return hasExcessProperties(source, discriminant, /*discriminant*/ undefined, reportErrors);
14792-
}
14793-
for (const prop of getPropertiesOfType(source)) {
14794-
if (shouldCheckAsExcessProperty(prop, source.symbol) && !isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) {
14766+
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
14767+
if ((relation === assignableRelation || relation === comparableRelation) &&
14768+
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) {
14769+
return false;
14770+
}
14771+
let reducedTarget = target;
14772+
let checkTypes: Type[] | undefined;
14773+
if (target.flags & TypeFlags.Union) {
14774+
reducedTarget = findMatchingDiscriminantType(source, <UnionType>target) || filterPrimitivesIfContainsNonPrimitive(<UnionType>target);
14775+
checkTypes = reducedTarget.flags & TypeFlags.Union ? (<UnionType>reducedTarget).types : [reducedTarget];
14776+
}
14777+
for (const prop of getPropertiesOfType(source)) {
14778+
if (shouldCheckAsExcessProperty(prop, source.symbol)) {
14779+
if (!isKnownProperty(reducedTarget, prop.escapedName, isComparingJsxAttributes)) {
1479514780
if (reportErrors) {
1479614781
// Report error in terms of object types in the target as those are the only ones
1479714782
// we check in isKnownProperty.
14798-
const errorTarget = filterType(target, isExcessPropertyCheckTarget);
14783+
const errorTarget = filterType(reducedTarget, isExcessPropertyCheckTarget);
1479914784
// We know *exactly* where things went wrong when comparing the types.
1480014785
// Use this property as the error node as this will be more helpful in
1480114786
// reasoning about what went wrong.
@@ -14826,7 +14811,6 @@ namespace ts {
1482614811
suggestion = getSuggestionForNonexistentProperty(name, errorTarget);
1482714812
}
1482814813
}
14829-
1483014814
if (suggestion !== undefined) {
1483114815
reportError(Diagnostics.Object_literal_may_only_specify_known_properties_but_0_does_not_exist_in_type_1_Did_you_mean_to_write_2,
1483214816
symbolToString(prop), typeToString(errorTarget), suggestion);
@@ -14839,6 +14823,12 @@ namespace ts {
1483914823
}
1484014824
return true;
1484114825
}
14826+
if (checkTypes && !isRelatedTo(getTypeOfSymbol(prop), getTypeOfPropertyInTypes(checkTypes, prop.escapedName), reportErrors)) {
14827+
if (reportErrors) {
14828+
reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(prop));
14829+
}
14830+
return true;
14831+
}
1484214832
}
1484314833
}
1484414834
return false;
@@ -15839,7 +15829,7 @@ namespace ts {
1583915829
}
1584015830
// We only call this for union target types when we're attempting to do excess property checking - in those cases, we want to get _all possible props_
1584115831
// from the target union, across all members
15842-
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
15832+
const properties = getPropertiesOfType(target);
1584315833
const numericNamesOnly = isTupleType(source) && isTupleType(target);
1584415834
for (const targetProp of excludeProperties(properties, excludedProperties)) {
1584515835
const name = targetProp.escapedName;
@@ -17348,7 +17338,7 @@ namespace ts {
1734817338
}
1734917339

1735017340
function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol> {
17351-
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
17341+
const properties = getPropertiesOfType(target);
1735217342
for (const targetProp of properties) {
1735317343
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial)) {
1735417344
const sourceProp = getPropertyOfType(source, targetProp.escapedName);

src/compiler/types.ts

-2
Original file line numberDiff line numberDiff line change
@@ -4484,8 +4484,6 @@ namespace ts {
44844484
}
44854485

44864486
export interface UnionType extends UnionOrIntersectionType {
4487-
/* @internal */
4488-
possiblePropertyCache?: SymbolTable; // Cache of _all_ resolved properties less any from aparent members
44894487
}
44904488

44914489
export interface IntersectionType extends UnionOrIntersectionType {

tests/baselines/reference/deepExcessPropertyCheckingWhenTargetIsIntersection.errors.txt

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
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 TS2200: The types of 'icon.props' are incompatible between these types.
4-
Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
5-
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'.
67

78

89
==== tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts (2 errors) ====
@@ -38,7 +39,8 @@ tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34
3839

3940
TestComponent2({icon: { props: { INVALID_PROP_NAME: 'share', ariaLabel: 'test label' } }});
4041
~~~~~~~~~~~~~~~~~~~~~~~~~~
41-
!!! error TS2200: The types of 'icon.props' are incompatible between these types.
42-
!!! error TS2200: Type '{ INVALID_PROP_NAME: string; ariaLabel: string; }' is not assignable to type 'ITestProps'.
43-
!!! error TS2200: Object literal may only specify known properties, and 'INVALID_PROP_NAME' does not exist in type 'ITestProps'.
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'.
4446

tests/baselines/reference/discriminateObjectTypesOnly.errors.txt

-17
This file was deleted.

tests/baselines/reference/errorsOnUnionsOfOverlappingObjects01.errors.txt

+4-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(18,3): error TS2345
33
Types of property 'b' are incompatible.
44
Type 'string' is not assignable to type 'number'.
55
tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(19,3): error TS2345: Argument of type '{ a: string; b: string; }' is not assignable to parameter of type 'Foo | Other'.
6-
Type '{ a: string; b: string; }' is not assignable to type 'Foo'.
7-
Types of property 'b' are incompatible.
8-
Type 'string' is not assignable to type 'number'.
6+
Types of property 'b' are incompatible.
7+
Type 'string' is not assignable to type 'number'.
98
tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(24,5): error TS2345: Argument of type '{ a: string; b: string; }' is not assignable to parameter of type 'Bar | Other'.
109
Object literal may only specify known properties, and 'a' does not exist in type 'Bar | Other'.
1110
tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(42,10): error TS2345: Argument of type '{ dog: string; }' is not assignable to parameter of type 'ExoticAnimal'.
@@ -45,9 +44,8 @@ tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(47,10): error TS234
4544
f({ a: '', b: '' })
4645
~~~~~~~~~~~~~~~~
4746
!!! error TS2345: Argument of type '{ a: string; b: string; }' is not assignable to parameter of type 'Foo | Other'.
48-
!!! error TS2345: Type '{ a: string; b: string; }' is not assignable to type 'Foo'.
49-
!!! error TS2345: Types of property 'b' are incompatible.
50-
!!! error TS2345: Type 'string' is not assignable to type 'number'.
47+
!!! error TS2345: Types of property 'b' are incompatible.
48+
!!! error TS2345: Type 'string' is not assignable to type 'number'.
5149

5250
declare function g(x: Bar | Other): any;
5351

0 commit comments

Comments
 (0)