mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-04 12:32:08 -06:00
Final fix: restrict heuristic to simple string literal unions only
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
parent
dd30988866
commit
31a298b0a2
@ -22890,59 +22890,63 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
|
||||
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
|
||||
}
|
||||
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
|
||||
if (
|
||||
(relation === assignableRelation || relation === comparableRelation) &&
|
||||
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heuristic: If the target type looks like a constraint (simple object type with few properties),
|
||||
// be more lenient with excess property checking. This handles cases like T extends { prop: Type }
|
||||
// where the constraint should allow additional properties.
|
||||
if (target.flags & TypeFlags.Object && !isComparingJsxAttributes) {
|
||||
const targetProperties = getPropertiesOfType(target);
|
||||
const targetIndexInfos = getIndexInfosOfType(target);
|
||||
// If it's a simple object with few properties and no index signatures, it might be a constraint
|
||||
if (targetProperties.length <= 3 && targetIndexInfos.length === 0) {
|
||||
// Additional check: at least one property should be a union type (common in constraints)
|
||||
// This helps distinguish constraints like { dataType: 'a' | 'b' } from regular types like { a: string }
|
||||
let hasUnionTypeProperty = false;
|
||||
for (const targetProp of targetProperties) {
|
||||
const propType = getTypeOfSymbol(targetProp);
|
||||
if (propType.flags & TypeFlags.Union) {
|
||||
hasUnionTypeProperty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUnionTypeProperty) {
|
||||
// Check if all properties in the target exist in the source
|
||||
let allTargetPropsExist = true;
|
||||
for (const targetProp of targetProperties) {
|
||||
if (!source.symbol?.members?.has(targetProp.escapedName)) {
|
||||
allTargetPropsExist = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the source contains all target properties, likely this is a constraint scenario
|
||||
if (allTargetPropsExist) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reducedTarget = target;
|
||||
let checkTypes: Type[] | undefined;
|
||||
if (target.flags & TypeFlags.Union) {
|
||||
reducedTarget = findMatchingDiscriminantType(source, target as UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as UnionType);
|
||||
checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget as UnionType).types : [reducedTarget];
|
||||
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
|
||||
}
|
||||
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
|
||||
if (
|
||||
(relation === assignableRelation || relation === comparableRelation) &&
|
||||
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heuristic: If the target type looks like a constraint (simple object type with few properties),
|
||||
// be more lenient with excess property checking. This handles cases like T extends { prop: Type }
|
||||
// where the constraint should allow additional properties.
|
||||
if (target.flags & TypeFlags.Object && !isComparingJsxAttributes) {
|
||||
const targetProperties = getPropertiesOfType(target);
|
||||
const targetIndexInfos = getIndexInfosOfType(target);
|
||||
// If it's a simple object with few properties and no index signatures, it might be a constraint
|
||||
if (targetProperties.length <= 2 && targetIndexInfos.length === 0) {
|
||||
// Additional check: at least one property should be a simple string literal union (common in constraints)
|
||||
// This helps distinguish constraints like { dataType: 'a' | 'b' } from complex intersection types
|
||||
let hasSimpleUnionProperty = false;
|
||||
for (const targetProp of targetProperties) {
|
||||
const propType = getTypeOfSymbol(targetProp);
|
||||
if (propType.flags & TypeFlags.Union) {
|
||||
const unionType = propType as UnionType;
|
||||
// Check if it's a union of string literals (typical of enum-like constraints)
|
||||
if (unionType.types.length <= 5 && unionType.types.every(t => t.flags & TypeFlags.StringLiteral)) {
|
||||
hasSimpleUnionProperty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSimpleUnionProperty) {
|
||||
// Check if all properties in the target exist in the source
|
||||
let allTargetPropsExist = true;
|
||||
for (const targetProp of targetProperties) {
|
||||
if (!source.symbol?.members?.has(targetProp.escapedName)) {
|
||||
allTargetPropsExist = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the source contains all target properties, likely this is a constraint scenario
|
||||
if (allTargetPropsExist) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reducedTarget = target;
|
||||
let checkTypes: Type[] | undefined;
|
||||
if (target.flags & TypeFlags.Union) {
|
||||
reducedTarget = findMatchingDiscriminantType(source, target as UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as UnionType);
|
||||
checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget as UnionType).types : [reducedTarget];
|
||||
}
|
||||
for (const prop of getPropertiesOfType(source)) {
|
||||
if (shouldCheckAsExcessProperty(prop, source.symbol) && !isIgnoredJsxProperty(source, prop)) {
|
||||
@ -34300,36 +34304,36 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
|
||||
* @param name a property name to search
|
||||
* @param isComparingJsxAttributes a boolean flag indicating whether we are searching in JsxAttributesType
|
||||
*/
|
||||
function isKnownProperty(targetType: Type, name: __String, isComparingJsxAttributes: boolean): boolean {
|
||||
if (targetType.flags & TypeFlags.Object) {
|
||||
// For backwards compatibility a symbol-named property is satisfied by a string index signature. This
|
||||
// is incorrect and inconsistent with element access expressions, where it is an error, so eventually
|
||||
// we should remove this exception.
|
||||
if (
|
||||
getPropertyOfObjectType(targetType, name) ||
|
||||
getApplicableIndexInfoForName(targetType, name) ||
|
||||
isLateBoundName(name) && getIndexInfoOfType(targetType, stringType) ||
|
||||
isComparingJsxAttributes && isHyphenatedJsxName(name)
|
||||
) {
|
||||
// For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (targetType.flags & TypeFlags.Substitution) {
|
||||
return isKnownProperty((targetType as SubstitutionType).baseType, name, isComparingJsxAttributes);
|
||||
}
|
||||
if (targetType.flags & TypeFlags.TypeParameter) {
|
||||
const constraint = getConstraintOfTypeParameter(targetType as TypeParameter);
|
||||
return constraint ? isKnownProperty(constraint, name, isComparingJsxAttributes) : false;
|
||||
}
|
||||
if (targetType.flags & TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) {
|
||||
for (const t of (targetType as UnionOrIntersectionType).types) {
|
||||
if (isKnownProperty(t, name, isComparingJsxAttributes)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
function isKnownProperty(targetType: Type, name: __String, isComparingJsxAttributes: boolean): boolean {
|
||||
if (targetType.flags & TypeFlags.Object) {
|
||||
// For backwards compatibility a symbol-named property is satisfied by a string index signature. This
|
||||
// is incorrect and inconsistent with element access expressions, where it is an error, so eventually
|
||||
// we should remove this exception.
|
||||
if (
|
||||
getPropertyOfObjectType(targetType, name) ||
|
||||
getApplicableIndexInfoForName(targetType, name) ||
|
||||
isLateBoundName(name) && getIndexInfoOfType(targetType, stringType) ||
|
||||
isComparingJsxAttributes && isHyphenatedJsxName(name)
|
||||
) {
|
||||
// For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (targetType.flags & TypeFlags.Substitution) {
|
||||
return isKnownProperty((targetType as SubstitutionType).baseType, name, isComparingJsxAttributes);
|
||||
}
|
||||
if (targetType.flags & TypeFlags.TypeParameter) {
|
||||
const constraint = getConstraintOfTypeParameter(targetType as TypeParameter);
|
||||
return constraint ? isKnownProperty(constraint, name, isComparingJsxAttributes) : false;
|
||||
}
|
||||
if (targetType.flags & TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) {
|
||||
for (const t of (targetType as UnionOrIntersectionType).types) {
|
||||
if (isKnownProperty(t, name, isComparingJsxAttributes)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isExcessPropertyCheckTarget(type: Type): boolean {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user