From 3ffd4f8698463d85a2a23f3a60e6ac0feab246c1 Mon Sep 17 00:00:00 2001 From: Alok Sharma Date: Sun, 27 Sep 2026 18:22:28 +0530 Subject: [PATCH] Attribute spread elaboration errors to the overriding spread, not the shadowed property When a later spread in an object literal overwrites an earlier explicit property of the same name in the merged type, elaborateObjectLiteral was still elaborating the assignability error against the earlier property's initializer. This pointed diagnostics at the wrong source location, since the spread - not the shadowed property - determines the property's final type. Fixes #51376 --- tsc/internal/checker/relater.go | 37 ++++++++++++-- ...idingPropertyElaboratesOnSpread.errors.txt | 33 +++++++++++++ ...erridingPropertyElaboratesOnSpread.symbols | 49 +++++++++++++++++++ ...OverridingPropertyElaboratesOnSpread.types | 49 +++++++++++++++++++ ...eadOverridingPropertyElaboratesOnSpread.ts | 22 +++++++++ 5 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.types create mode 100644 tsc/testdata/tests/cases/compiler/spreadOverridingPropertyElaboratesOnSpread.ts diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 9d71d2504ca0c..237ef50b85dbc 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -497,8 +497,24 @@ func (c *Checker) elaborateObjectLiteral(node *ast.Node, source *Type, target *T if target.flags&(TypeFlagsPrimitive|TypeFlagsNever) != 0 { return false } + properties := node.Properties() + spreadNamesByIndex := make(map[int]collections.Set[string]) + for i, prop := range properties { + if !ast.IsSpreadAssignment(prop) { + continue + } + spreadType := c.getReducedType(c.checkExpressionEx(prop.Expression(), CheckModeNormal)) + if !c.isValidSpreadType(spreadType) { + continue + } + names := collections.Set[string]{} + for _, p := range c.getPropertiesOfType(spreadType) { + names.Add(p.Name) + } + spreadNamesByIndex[i] = names + } reportedError := false - for _, prop := range node.Properties() { + for i, prop := range properties { if ast.IsSpreadAssignment(prop) { continue } @@ -506,12 +522,27 @@ func (c *Checker) elaborateObjectLiteral(node *ast.Node, source *Type, target *T if nameType == nil || nameType.flags&TypeFlagsNever != 0 { continue } + anchor := prop.Name() + var next *ast.Node + if ast.IsPropertyAssignment(prop) { + next = prop.Initializer() + } + if isTypeUsableAsPropertyName(nameType) { + name := getPropertyNameFromType(nameType) + for j := i + 1; j < len(properties); j++ { + if names, ok := spreadNamesByIndex[j]; ok && names.Has(name) { + anchor = properties[j].Expression() + next = nil + break + } + } + } switch prop.Kind { case ast.KindSetAccessor, ast.KindGetAccessor, ast.KindMethodDeclaration, ast.KindShorthandPropertyAssignment: - reportedError = c.elaborateElement(source, target, relation, prop.Name(), nil, nameType, nil, nil, diagnosticOutput) || reportedError + reportedError = c.elaborateElement(source, target, relation, anchor, nil, nameType, nil, nil, diagnosticOutput) || reportedError case ast.KindPropertyAssignment: message := core.IfElse(ast.IsComputedNonLiteralName(prop.Name()), diagnostics.Type_of_computed_property_s_value_is_0_which_is_not_assignable_to_type_1, nil) - reportedError = c.elaborateElement(source, target, relation, prop.Name(), prop.Initializer(), nameType, message, nil, diagnosticOutput) || reportedError + reportedError = c.elaborateElement(source, target, relation, anchor, next, nameType, message, nil, diagnosticOutput) || reportedError } } return reportedError diff --git a/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.errors.txt b/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.errors.txt new file mode 100644 index 0000000000000..6dd30fe4778e2 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.errors.txt @@ -0,0 +1,33 @@ +spreadOverridingPropertyElaboratesOnSpread.ts(9,8): error TS2322: Type 'string | null' is not assignable to type 'string'. + Type 'null' is not assignable to type 'string'. +spreadOverridingPropertyElaboratesOnSpread.ts(18,5): error TS2322: Type 'null' is not assignable to type 'string'. + + +==== spreadOverridingPropertyElaboratesOnSpread.ts (2 errors) ==== + type Item = { + attribute: string; + }; + + declare const result: Partial>; + + const item: Item = { + attribute: "", + ...result, + ~~~~~~ +!!! error TS2322: Type 'string | null' is not assignable to type 'string'. +!!! error TS2322: Type 'null' is not assignable to type 'string'. +!!! related TS6500 spreadOverridingPropertyElaboratesOnSpread.ts:2:5: The expected type comes from property 'attribute' which is declared here on type 'Item' + }; + + const item2: Item = { + ...result, + attribute: "", + }; + + const item3: Item = { + attribute: null, + ~~~~~~~~~ +!!! error TS2322: Type 'null' is not assignable to type 'string'. +!!! related TS6500 spreadOverridingPropertyElaboratesOnSpread.ts:2:5: The expected type comes from property 'attribute' which is declared here on type 'Item' + }; + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.symbols b/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.symbols new file mode 100644 index 0000000000000..75fa81c8893a2 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.symbols @@ -0,0 +1,49 @@ +//// [tests/cases/compiler/spreadOverridingPropertyElaboratesOnSpread.ts] //// + +=== spreadOverridingPropertyElaboratesOnSpread.ts === +type Item = { +>Item : Symbol(Item, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 0, 0)) + + attribute: string; +>attribute : Symbol(attribute, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 0, 13)) + +}; + +declare const result: Partial>; +>result : Symbol(result, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 4, 13)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + +const item: Item = { +>item : Symbol(item, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 6, 5)) +>Item : Symbol(Item, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 0, 0)) + + attribute: "", +>attribute : Symbol(attribute, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 6, 20)) + + ...result, +>result : Symbol(result, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 4, 13)) + +}; + +const item2: Item = { +>item2 : Symbol(item2, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 11, 5)) +>Item : Symbol(Item, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 0, 0)) + + ...result, +>result : Symbol(result, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 4, 13)) + + attribute: "", +>attribute : Symbol(attribute, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 12, 14)) + +}; + +const item3: Item = { +>item3 : Symbol(item3, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 16, 5)) +>Item : Symbol(Item, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 0, 0)) + + attribute: null, +>attribute : Symbol(attribute, Decl(spreadOverridingPropertyElaboratesOnSpread.ts, 16, 21)) + +}; + diff --git a/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.types b/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.types new file mode 100644 index 0000000000000..b8ea404162d3f --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/spreadOverridingPropertyElaboratesOnSpread.types @@ -0,0 +1,49 @@ +//// [tests/cases/compiler/spreadOverridingPropertyElaboratesOnSpread.ts] //// + +=== spreadOverridingPropertyElaboratesOnSpread.ts === +type Item = { +>Item : Item + + attribute: string; +>attribute : string + +}; + +declare const result: Partial>; +>result : Partial> + +const item: Item = { +>item : Item +>{ attribute: "", ...result,} : { attribute: string | null; } + + attribute: "", +>attribute : string +>"" : "" + + ...result, +>result : Partial> + +}; + +const item2: Item = { +>item2 : Item +>{ ...result, attribute: "",} : { attribute: string; } + + ...result, +>result : Partial> + + attribute: "", +>attribute : string +>"" : "" + +}; + +const item3: Item = { +>item3 : Item +>{ attribute: null,} : { attribute: null; } + + attribute: null, +>attribute : null + +}; + diff --git a/tsc/testdata/tests/cases/compiler/spreadOverridingPropertyElaboratesOnSpread.ts b/tsc/testdata/tests/cases/compiler/spreadOverridingPropertyElaboratesOnSpread.ts new file mode 100644 index 0000000000000..1dc86696fa964 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/spreadOverridingPropertyElaboratesOnSpread.ts @@ -0,0 +1,22 @@ +// @strict: true +// @noEmit: true + +type Item = { + attribute: string; +}; + +declare const result: Partial>; + +const item: Item = { + attribute: "", + ...result, +}; + +const item2: Item = { + ...result, + attribute: "", +}; + +const item3: Item = { + attribute: null, +};