diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f3db3f22bf2..2d1aaedf0fc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13356,9 +13356,7 @@ namespace ts { */ function createJsxAttributesType(symbol: Symbol, attributesTable: Map) { const result = createAnonymousType(symbol, attributesTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined); - // Spread object doesn't have freshness flag to allow excess attributes as it is very common for parent component to spread its "props" to other components in its render method. - const freshObjectLiteralFlag = spread !== emptyObjectType || compilerOptions.suppressExcessPropertyErrors ? 0 : TypeFlags.FreshLiteral; - result.flags |= TypeFlags.JsxAttributes | TypeFlags.ContainsObjectLiteral | freshObjectLiteralFlag; + result.flags |= TypeFlags.JsxAttributes | TypeFlags.ContainsObjectLiteral; result.objectFlags |= ObjectFlags.ObjectLiteral; return result; } @@ -13877,7 +13875,30 @@ namespace ts { checkJsxAttributesAssignableToTagNameAttributes(node); } - /** + // Check if a property with the given name is known anywhere in the given type. In an object type, a property + // is considered known if the object type is empty and the check is for assignability, if the object type has + // index signatures, or if the property is actually declared in the object type. In a union or intersection + // type, a property is considered known if it is known in any constituent type. + function isKnownProperty(type: Type, name: string, isComparingJsxAttributes: boolean): boolean { + if (type.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers(type); + if (resolved.stringIndexInfo || resolved.numberIndexInfo && isNumericLiteralName(name) || + getPropertyOfType(type, name) || isComparingJsxAttributes && !isUnhyphenatedJsxName(name)) { + // For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known. + return true; + } + } + else if (type.flags & TypeFlags.UnionOrIntersection) { + for (const t of (type).types) { + if (isKnownProperty(t, name, isComparingJsxAttributes)) { + return true; + } + } + } + return false; + } + + /** * Check whether the given attributes of JSX opening-like element is assignable to the tagName attributes. * Get the attributes type of the opening-like element through resolving the tagName, "target attributes" * Check assignablity between given attributes property, "source attributes", and the "target attributes" @@ -13910,12 +13931,12 @@ namespace ts { } else { const isSourceAttributeTypeAssignableToTarget = checkTypeAssignableTo(sourceAttributesType, targetAttributesType, openingLikeElement.attributes.properties.length > 0 ? openingLikeElement.attributes : openingLikeElement); - // If sourceAttributesType has spread (e.g the type doesn't have freshness flag) after we check for assignability, we will do another pass to check that + // After we check for assignability, we will do another pass to check that // all explicitly specified attributes have correct name corresponding with target (as those will be assignable as spread type allows excess properties) // Note: if the type of these explicitly specified attributes do not match it will be an error during above assignability check. - if (isSourceAttributeTypeAssignableToTarget && sourceAttributesType !== anyType && !(sourceAttributesType.flags & TypeFlags.FreshLiteral)) { + if (isSourceAttributeTypeAssignableToTarget && !isTypeAny(sourceAttributesType) && !isTypeAny(targetAttributesType)) { for (const attribute of openingLikeElement.attributes.properties) { - if (isJsxAttribute(attribute) && !getPropertyOfType(targetAttributesType, attribute.name.text)) { + if (isJsxAttribute(attribute) && !isKnownProperty(targetAttributesType, attribute.name.text, /*isComparingJsxAttributes*/ true)) { error(attribute, Diagnostics.Property_0_does_not_exist_on_type_1, attribute.name.text, typeToString(targetAttributesType)); } }