Skip to content

Fix excess property checking for unions with index signatures #34927

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
merged 5 commits into from
Nov 15, 2019
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
82 changes: 36 additions & 46 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9370,25 +9370,6 @@ namespace ts {
return type.resolvedProperties;
}

function getPossiblePropertiesOfUnionType(type: UnionType): Symbol[] {
if (type.possiblePropertyCache) {
return type.possiblePropertyCache.size ? arrayFrom(type.possiblePropertyCache.values()) : emptyArray;
}
type.possiblePropertyCache = createSymbolTable();
for (const t of type.types) {
for (const p of getPropertiesOfType(t)) {
if (!type.possiblePropertyCache.has(p.escapedName)) {
const prop = getUnionOrIntersectionProperty(type, p.escapedName);
if (prop) {
type.possiblePropertyCache.set(p.escapedName, prop);
}
}
}
}
// We can't simply use the normal property cache here, since that will contain cached apparent type members :(
return type.possiblePropertyCache.size ? arrayFrom(type.possiblePropertyCache.values()) : emptyArray;
}

function getPropertiesOfType(type: Type): Symbol[] {
type = getApparentType(type);
return type.flags & TypeFlags.UnionOrIntersection ?
Expand Down Expand Up @@ -14614,8 +14595,7 @@ namespace ts {
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
const isPerformingExcessPropertyChecks = !isApparentIntersectionConstituent && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
if (isPerformingExcessPropertyChecks) {
const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined;
if (hasExcessProperties(<FreshObjectLiteralType>source, target, discriminantType, reportErrors)) {
if (hasExcessProperties(<FreshObjectLiteralType>source, target, reportErrors)) {
if (reportErrors) {
reportRelationError(headMessage, source, target);
}
Expand Down Expand Up @@ -14657,13 +14637,6 @@ namespace ts {
else {
if (target.flags & TypeFlags.Union) {
result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks)) {
// Validate against excess props using the original `source`
const discriminantType = findMatchingDiscriminantType(source, target as UnionType) || filterPrimitivesIfContainsNonPrimitive(target as UnionType);
if (!propertiesRelatedTo(source, discriminantType, reportErrors, /*excludedProperties*/ undefined, isIntersectionConstituent)) {
return Ternary.False;
}
}
}
else if (target.flags & TypeFlags.Intersection) {
isIntersectionConstituent = true; // set here to affect the following trio of checks
Expand Down Expand Up @@ -14776,26 +14749,38 @@ namespace ts {
return Ternary.False;
}

function hasExcessProperties(source: FreshObjectLiteralType, target: Type, discriminant: Type | undefined, reportErrors: boolean): boolean {
if (!noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) {
function getTypeOfPropertyInTypes(types: Type[], name: __String) {
const appendPropType = (propTypes: Type[] | undefined, type: Type) => {
type = getApparentType(type);
const prop = type.flags & TypeFlags.UnionOrIntersection ? getPropertyOfUnionOrIntersectionType(<UnionOrIntersectionType>type, name) : getPropertyOfObjectType(type, name);
const propType = prop && getTypeOfSymbol(prop) || isNumericLiteralName(name) && getIndexTypeOfType(type, IndexKind.Number) || getIndexTypeOfType(type, IndexKind.String) || undefinedType;
return append(propTypes, propType);
};
return getUnionType(reduceLeft(types, appendPropType, /*initial*/ undefined) || emptyArray);
}

function hasExcessProperties(source: FreshObjectLiteralType, target: Type, reportErrors: boolean): boolean {
if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) {
return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny
}
if (isExcessPropertyCheckTarget(target)) {
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
if ((relation === assignableRelation || relation === comparableRelation) &&
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) {
return false;
}
if (discriminant) {
// check excess properties against discriminant type only, not the entire union
return hasExcessProperties(source, discriminant, /*discriminant*/ undefined, reportErrors);
}
for (const prop of getPropertiesOfType(source)) {
if (shouldCheckAsExcessProperty(prop, source.symbol) && !isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) {
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
if ((relation === assignableRelation || relation === comparableRelation) &&
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) {
return false;
}
let reducedTarget = target;
let checkTypes: Type[] | undefined;
if (target.flags & TypeFlags.Union) {
reducedTarget = findMatchingDiscriminantType(source, <UnionType>target) || filterPrimitivesIfContainsNonPrimitive(<UnionType>target);
checkTypes = reducedTarget.flags & TypeFlags.Union ? (<UnionType>reducedTarget).types : [reducedTarget];
}
for (const prop of getPropertiesOfType(source)) {
if (shouldCheckAsExcessProperty(prop, source.symbol)) {
if (!isKnownProperty(reducedTarget, prop.escapedName, isComparingJsxAttributes)) {
if (reportErrors) {
// Report error in terms of object types in the target as those are the only ones
// we check in isKnownProperty.
const errorTarget = filterType(target, isExcessPropertyCheckTarget);
const errorTarget = filterType(reducedTarget, isExcessPropertyCheckTarget);
// We know *exactly* where things went wrong when comparing the types.
// Use this property as the error node as this will be more helpful in
// reasoning about what went wrong.
Expand Down Expand Up @@ -14826,7 +14811,6 @@ namespace ts {
suggestion = getSuggestionForNonexistentProperty(name, errorTarget);
}
}

if (suggestion !== undefined) {
reportError(Diagnostics.Object_literal_may_only_specify_known_properties_but_0_does_not_exist_in_type_1_Did_you_mean_to_write_2,
symbolToString(prop), typeToString(errorTarget), suggestion);
Expand All @@ -14839,6 +14823,12 @@ namespace ts {
}
return true;
}
if (checkTypes && !isRelatedTo(getTypeOfSymbol(prop), getTypeOfPropertyInTypes(checkTypes, prop.escapedName), reportErrors)) {
if (reportErrors) {
reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(prop));
}
return true;
}
}
}
return false;
Expand Down Expand Up @@ -15839,7 +15829,7 @@ namespace ts {
}
// 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_
// from the target union, across all members
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
const properties = getPropertiesOfType(target);
const numericNamesOnly = isTupleType(source) && isTupleType(target);
for (const targetProp of excludeProperties(properties, excludedProperties)) {
const name = targetProp.escapedName;
Expand Down Expand Up @@ -17344,7 +17334,7 @@ namespace ts {
}

function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol> {
const properties = target.flags & TypeFlags.Union ? getPossiblePropertiesOfUnionType(target as UnionType) : getPropertiesOfType(target);
const properties = getPropertiesOfType(target);
for (const targetProp of properties) {
if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial)) {
const sourceProp = getPropertyOfType(source, targetProp.escapedName);
Expand Down
2 changes: 0 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4483,8 +4483,6 @@ namespace ts {
}

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

export interface IntersectionType extends UnionOrIntersectionType {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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 TS2200: 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 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 (2 errors) ====
Expand Down Expand Up @@ -38,7 +39,8 @@ tests/cases/compiler/deepExcessPropertyCheckingWhenTargetIsIntersection.ts(27,34

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

@weswigham weswigham Nov 6, 2019

Choose a reason for hiding this comment

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

Should we try and keep this line of elaboration suppressed? I think the filtering we were doing before had the effect of suppressing it.

!!! 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'.

17 changes: 0 additions & 17 deletions tests/baselines/reference/discriminateObjectTypesOnly.errors.txt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(18,3): error TS2345
Types of property 'b' are incompatible.
Type 'string' is not assignable to type 'number'.
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'.
Type '{ a: string; b: string; }' is not assignable to type 'Foo'.
Types of property 'b' are incompatible.
Type 'string' is not assignable to type 'number'.
Types of property 'b' are incompatible.
Type 'string' is not assignable to type 'number'.
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'.
Object literal may only specify known properties, and 'a' does not exist in type 'Bar | Other'.
tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(42,10): error TS2345: Argument of type '{ dog: string; }' is not assignable to parameter of type 'ExoticAnimal'.
Expand Down Expand Up @@ -45,9 +44,8 @@ tests/cases/compiler/errorsOnUnionsOfOverlappingObjects01.ts(47,10): error TS234
f({ a: '', b: '' })
~~~~~~~~~~~~~~~~
!!! error TS2345: Argument of type '{ a: string; b: string; }' is not assignable to parameter of type 'Foo | Other'.
!!! error TS2345: Type '{ a: string; b: string; }' is not assignable to type 'Foo'.
!!! error TS2345: Types of property 'b' are incompatible.
!!! error TS2345: Type 'string' is not assignable to type 'number'.
!!! error TS2345: Types of property 'b' are incompatible.
!!! error TS2345: Type 'string' is not assignable to type 'number'.

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

Expand Down
Loading