From 9fbcee2d0ce01924177eac777ce6521b72700f5a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:40:30 -0700 Subject: [PATCH 1/2] Align object binding defaults with arrays Object binding patterns with an object-literal parameter default only added missing elements that had their own initializer. Array patterns already add every missing element, so equivalent object and array parameters inferred different types and produced different diagnostics. Pad object literal defaults with every missing binding element in all source kinds. Report implicit any through the existing diagnostic path, which preserves normal checkJs suppression while making checked JS and TypeScript match array patterns. Add matrix coverage for object and array patterns in TypeScript, checked JavaScript, and unchecked JavaScript. Fixes #59920 --- tsc/internal/checker/checker.go | 10 +- .../inferredRestTypeFixedOnce.errors.txt | 9 ++ ...ngPatternDefaultMissingElements.errors.txt | 58 ++++++++ ...ndingPatternDefaultMissingElements.symbols | 101 +++++++++++++ ...BindingPatternDefaultMissingElements.types | 137 ++++++++++++++++++ ...ectBindingPatternDefaultMissingElements.ts | 48 ++++++ 6 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/inferredRestTypeFixedOnce.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types create mode 100644 tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 9c9cb64c78a31..f2878d6bdc789 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -16918,11 +16918,9 @@ func (c *Checker) checkDeclarationInitializer(declaration *ast.Node, checkMode C func (c *Checker) padObjectLiteralType(t *Type, pattern *ast.Node) *Type { var missingElements []*ast.Node for _, e := range pattern.Elements() { - if e.Initializer() != nil { - name := c.getPropertyNameFromBindingElement(e) - if name != ast.InternalSymbolNameMissing && c.getPropertyOfType(t, name) == nil { - missingElements = append(missingElements, e) - } + name := c.getPropertyNameFromBindingElement(e) + if name != ast.InternalSymbolNameMissing && c.getPropertyOfType(t, name) == nil { + missingElements = append(missingElements, e) } } if len(missingElements) == 0 { @@ -16934,7 +16932,7 @@ func (c *Checker) padObjectLiteralType(t *Type, pattern *ast.Node) *Type { } for _, e := range missingElements { symbol := c.newSymbol(ast.SymbolFlagsProperty|ast.SymbolFlagsOptional, c.getPropertyNameFromBindingElement(e)) - c.valueSymbolLinks.Get(symbol).resolvedType = c.getTypeFromBindingElement(e, false /*includePatternInType*/, false /*reportErrors*/) + c.valueSymbolLinks.Get(symbol).resolvedType = c.getTypeFromBindingElement(e, false /*includePatternInType*/, true /*reportErrors*/) members[symbol.Name] = symbol } result := c.newAnonymousType(t.symbol, members, nil, nil, c.getIndexInfosOfType(t)) diff --git a/tsc/testdata/baselines/reference/compiler/inferredRestTypeFixedOnce.errors.txt b/tsc/testdata/baselines/reference/compiler/inferredRestTypeFixedOnce.errors.txt new file mode 100644 index 0000000000000..d310258fda658 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/inferredRestTypeFixedOnce.errors.txt @@ -0,0 +1,9 @@ +inferredRestTypeFixedOnce.ts(2,9): error TS7031: Binding element 'cancelable' implicitly has an 'any' type. + + +==== inferredRestTypeFixedOnce.ts (1 errors) ==== + function wrap(_: (...args: Args) => void) {} + wrap(({ cancelable } = {}) => {}); + ~~~~~~~~~~ +!!! error TS7031: Binding element 'cancelable' implicitly has an 'any' type. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt new file mode 100644 index 0000000000000..a8785d4baa29b --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt @@ -0,0 +1,58 @@ +/checked.js(3,5): error TS7031: Binding element 'required' implicitly has an 'any' type. +/checked.js(8,5): error TS7031: Binding element 'required' implicitly has an 'any' type. +/main.ts(5,5): error TS7031: Binding element 'required' implicitly has an 'any' type. +/main.ts(10,5): error TS7031: Binding element 'required' implicitly has an 'any' type. + + +==== /unchecked.js (0 errors) ==== + export const uncheckedObject = ({ + required, + optional = false, + } = {}) => {}; + + export const uncheckedArray = ([ + required, + optional = false, + ] = []) => {}; + +==== /checked.js (2 errors) ==== + // @ts-check + export const checkedObject = ({ + required, + ~~~~~~~~ +!!! error TS7031: Binding element 'required' implicitly has an 'any' type. + optional = false, + } = {}) => {}; + + export const checkedArray = ([ + required, + ~~~~~~~~ +!!! error TS7031: Binding element 'required' implicitly has an 'any' type. + optional = false, + ] = []) => {}; + +==== /main.ts (2 errors) ==== + import { checkedArray, checkedObject } from "./checked"; + import { uncheckedArray, uncheckedObject } from "./unchecked"; + + export const typedObject = ({ + required, + ~~~~~~~~ +!!! error TS7031: Binding element 'required' implicitly has an 'any' type. + optional = false, + } = {}) => {}; + + export const typedArray = ([ + required, + ~~~~~~~~ +!!! error TS7031: Binding element 'required' implicitly has an 'any' type. + optional = false, + ] = []) => {}; + + uncheckedObject({ required: "value" }); + uncheckedArray(["value"]); + checkedObject({ required: "value" }); + checkedArray(["value"]); + typedObject({ required: "value" }); + typedArray(["value"]); + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols new file mode 100644 index 0000000000000..3569cdca01542 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols @@ -0,0 +1,101 @@ +//// [tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts] //// + +=== /unchecked.js === +export const uncheckedObject = ({ +>uncheckedObject : Symbol(uncheckedObject, Decl(unchecked.js, 0, 12)) + + required, +>required : Symbol(required, Decl(unchecked.js, 0, 33)) + + optional = false, +>optional : Symbol(optional, Decl(unchecked.js, 1, 13)) + +} = {}) => {}; + +export const uncheckedArray = ([ +>uncheckedArray : Symbol(uncheckedArray, Decl(unchecked.js, 5, 12)) + + required, +>required : Symbol(required, Decl(unchecked.js, 5, 32)) + + optional = false, +>optional : Symbol(optional, Decl(unchecked.js, 6, 13)) + +] = []) => {}; + +=== /checked.js === +// @ts-check +export const checkedObject = ({ +>checkedObject : Symbol(checkedObject, Decl(checked.js, 1, 12)) + + required, +>required : Symbol(required, Decl(checked.js, 1, 31)) + + optional = false, +>optional : Symbol(optional, Decl(checked.js, 2, 13)) + +} = {}) => {}; + +export const checkedArray = ([ +>checkedArray : Symbol(checkedArray, Decl(checked.js, 6, 12)) + + required, +>required : Symbol(required, Decl(checked.js, 6, 30)) + + optional = false, +>optional : Symbol(optional, Decl(checked.js, 7, 13)) + +] = []) => {}; + +=== /main.ts === +import { checkedArray, checkedObject } from "./checked"; +>checkedArray : Symbol(checkedArray, Decl(main.ts, 0, 8)) +>checkedObject : Symbol(checkedObject, Decl(main.ts, 0, 22)) + +import { uncheckedArray, uncheckedObject } from "./unchecked"; +>uncheckedArray : Symbol(uncheckedArray, Decl(main.ts, 1, 8)) +>uncheckedObject : Symbol(uncheckedObject, Decl(main.ts, 1, 24)) + +export const typedObject = ({ +>typedObject : Symbol(typedObject, Decl(main.ts, 3, 12)) + + required, +>required : Symbol(required, Decl(main.ts, 3, 29)) + + optional = false, +>optional : Symbol(optional, Decl(main.ts, 4, 13)) + +} = {}) => {}; + +export const typedArray = ([ +>typedArray : Symbol(typedArray, Decl(main.ts, 8, 12)) + + required, +>required : Symbol(required, Decl(main.ts, 8, 28)) + + optional = false, +>optional : Symbol(optional, Decl(main.ts, 9, 13)) + +] = []) => {}; + +uncheckedObject({ required: "value" }); +>uncheckedObject : Symbol(uncheckedObject, Decl(main.ts, 1, 24)) +>required : Symbol(required, Decl(main.ts, 13, 17)) + +uncheckedArray(["value"]); +>uncheckedArray : Symbol(uncheckedArray, Decl(main.ts, 1, 8)) + +checkedObject({ required: "value" }); +>checkedObject : Symbol(checkedObject, Decl(main.ts, 0, 22)) +>required : Symbol(required, Decl(main.ts, 15, 15)) + +checkedArray(["value"]); +>checkedArray : Symbol(checkedArray, Decl(main.ts, 0, 8)) + +typedObject({ required: "value" }); +>typedObject : Symbol(typedObject, Decl(main.ts, 3, 12)) +>required : Symbol(required, Decl(main.ts, 17, 13)) + +typedArray(["value"]); +>typedArray : Symbol(typedArray, Decl(main.ts, 8, 12)) + diff --git a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types new file mode 100644 index 0000000000000..b56d0c36b3ab6 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types @@ -0,0 +1,137 @@ +//// [tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts] //// + +=== /unchecked.js === +export const uncheckedObject = ({ +>uncheckedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void +>({ required, optional = false,} = {}) => {} : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void + + required, +>required : any + + optional = false, +>optional : boolean +>false : false + +} = {}) => {}; +>{} : {} + +export const uncheckedArray = ([ +>uncheckedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>([ required, optional = false,] = []) => {} : ([required, optional,]?: [any?, (boolean | undefined)?]) => void + + required, +>required : any + + optional = false, +>optional : boolean +>false : false + +] = []) => {}; +>[] : [] + +=== /checked.js === +// @ts-check +export const checkedObject = ({ +>checkedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void +>({ required, optional = false,} = {}) => {} : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void + + required, +>required : any + + optional = false, +>optional : boolean +>false : false + +} = {}) => {}; +>{} : {} + +export const checkedArray = ([ +>checkedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>([ required, optional = false,] = []) => {} : ([required, optional,]?: [any?, (boolean | undefined)?]) => void + + required, +>required : any + + optional = false, +>optional : boolean +>false : false + +] = []) => {}; +>[] : [] + +=== /main.ts === +import { checkedArray, checkedObject } from "./checked"; +>checkedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>checkedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void + +import { uncheckedArray, uncheckedObject } from "./unchecked"; +>uncheckedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>uncheckedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void + +export const typedObject = ({ +>typedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void +>({ required, optional = false,} = {}) => {} : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void + + required, +>required : any + + optional = false, +>optional : boolean +>false : false + +} = {}) => {}; +>{} : {} + +export const typedArray = ([ +>typedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>([ required, optional = false,] = []) => {} : ([required, optional,]?: [any?, (boolean | undefined)?]) => void + + required, +>required : any + + optional = false, +>optional : boolean +>false : false + +] = []) => {}; +>[] : [] + +uncheckedObject({ required: "value" }); +>uncheckedObject({ required: "value" }) : void +>uncheckedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void +>{ required: "value" } : { required: string; } +>required : string +>"value" : "value" + +uncheckedArray(["value"]); +>uncheckedArray(["value"]) : void +>uncheckedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>["value"] : [string] +>"value" : "value" + +checkedObject({ required: "value" }); +>checkedObject({ required: "value" }) : void +>checkedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void +>{ required: "value" } : { required: string; } +>required : string +>"value" : "value" + +checkedArray(["value"]); +>checkedArray(["value"]) : void +>checkedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>["value"] : [string] +>"value" : "value" + +typedObject({ required: "value" }); +>typedObject({ required: "value" }) : void +>typedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void +>{ required: "value" } : { required: string; } +>required : string +>"value" : "value" + +typedArray(["value"]); +>typedArray(["value"]) : void +>typedArray : ([required, optional,]?: [any?, (boolean | undefined)?]) => void +>["value"] : [string] +>"value" : "value" + diff --git a/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts b/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts new file mode 100644 index 0000000000000..8586a95216b20 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts @@ -0,0 +1,48 @@ +// @allowJs: true +// @checkJs: false +// @noEmit: true +// @strict: true + +// @filename: /unchecked.js +export const uncheckedObject = ({ + required, + optional = false, +} = {}) => {}; + +export const uncheckedArray = ([ + required, + optional = false, +] = []) => {}; + +// @filename: /checked.js +// @ts-check +export const checkedObject = ({ + required, + optional = false, +} = {}) => {}; + +export const checkedArray = ([ + required, + optional = false, +] = []) => {}; + +// @filename: /main.ts +import { checkedArray, checkedObject } from "./checked"; +import { uncheckedArray, uncheckedObject } from "./unchecked"; + +export const typedObject = ({ + required, + optional = false, +} = {}) => {}; + +export const typedArray = ([ + required, + optional = false, +] = []) => {}; + +uncheckedObject({ required: "value" }); +uncheckedArray(["value"]); +checkedObject({ required: "value" }); +checkedArray(["value"]); +typedObject({ required: "value" }); +typedArray(["value"]); From d0af0f0f66f83503a05bd22b01fab2ce6266800a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:44:23 -0700 Subject: [PATCH 2/2] Skip object rest elements when padding defaults Object binding padding now considers every missing binding element, but a rest binding does not name a source property. Treating it as a property synthesized an optional member with the rest variable name. That made the rest variable contain itself and produced a spurious implicit-any diagnostic. Skip rest elements before looking up missing properties, matching object binding-pattern inference. Add coverage for the inferred parameter and rest-variable types and for the remaining required binding diagnostic. --- tsc/internal/checker/checker.go | 3 +++ ...ngPatternDefaultMissingElements.errors.txt | 12 ++++++++++- ...ndingPatternDefaultMissingElements.symbols | 21 ++++++++++++++++--- ...BindingPatternDefaultMissingElements.types | 18 ++++++++++++++++ ...ectBindingPatternDefaultMissingElements.ts | 7 +++++++ 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index f2878d6bdc789..e2c271f65eff3 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -16918,6 +16918,9 @@ func (c *Checker) checkDeclarationInitializer(declaration *ast.Node, checkMode C func (c *Checker) padObjectLiteralType(t *Type, pattern *ast.Node) *Type { var missingElements []*ast.Node for _, e := range pattern.Elements() { + if hasDotDotDotToken(e) { + continue + } name := c.getPropertyNameFromBindingElement(e) if name != ast.InternalSymbolNameMissing && c.getPropertyOfType(t, name) == nil { missingElements = append(missingElements, e) diff --git a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt index a8785d4baa29b..823bd410ef0cb 100644 --- a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt +++ b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.errors.txt @@ -2,6 +2,7 @@ /checked.js(8,5): error TS7031: Binding element 'required' implicitly has an 'any' type. /main.ts(5,5): error TS7031: Binding element 'required' implicitly has an 'any' type. /main.ts(10,5): error TS7031: Binding element 'required' implicitly has an 'any' type. +/main.ts(15,5): error TS7031: Binding element 'required' implicitly has an 'any' type. ==== /unchecked.js (0 errors) ==== @@ -31,7 +32,7 @@ optional = false, ] = []) => {}; -==== /main.ts (2 errors) ==== +==== /main.ts (3 errors) ==== import { checkedArray, checkedObject } from "./checked"; import { uncheckedArray, uncheckedObject } from "./unchecked"; @@ -49,6 +50,15 @@ optional = false, ] = []) => {}; + export const typedObjectRest = ({ + required, + ~~~~~~~~ +!!! error TS7031: Binding element 'required' implicitly has an 'any' type. + ...rest + } = {}) => { + rest; + }; + uncheckedObject({ required: "value" }); uncheckedArray(["value"]); checkedObject({ required: "value" }); diff --git a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols index 3569cdca01542..5542ea8d095bf 100644 --- a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols +++ b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.symbols @@ -78,23 +78,38 @@ export const typedArray = ([ ] = []) => {}; +export const typedObjectRest = ({ +>typedObjectRest : Symbol(typedObjectRest, Decl(main.ts, 13, 12)) + + required, +>required : Symbol(required, Decl(main.ts, 13, 33)) + + ...rest +>rest : Symbol(rest, Decl(main.ts, 14, 13)) + +} = {}) => { + rest; +>rest : Symbol(rest, Decl(main.ts, 14, 13)) + +}; + uncheckedObject({ required: "value" }); >uncheckedObject : Symbol(uncheckedObject, Decl(main.ts, 1, 24)) ->required : Symbol(required, Decl(main.ts, 13, 17)) +>required : Symbol(required, Decl(main.ts, 20, 17)) uncheckedArray(["value"]); >uncheckedArray : Symbol(uncheckedArray, Decl(main.ts, 1, 8)) checkedObject({ required: "value" }); >checkedObject : Symbol(checkedObject, Decl(main.ts, 0, 22)) ->required : Symbol(required, Decl(main.ts, 15, 15)) +>required : Symbol(required, Decl(main.ts, 22, 15)) checkedArray(["value"]); >checkedArray : Symbol(checkedArray, Decl(main.ts, 0, 8)) typedObject({ required: "value" }); >typedObject : Symbol(typedObject, Decl(main.ts, 3, 12)) ->required : Symbol(required, Decl(main.ts, 17, 13)) +>required : Symbol(required, Decl(main.ts, 24, 13)) typedArray(["value"]); >typedArray : Symbol(typedArray, Decl(main.ts, 8, 12)) diff --git a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types index b56d0c36b3ab6..1791cbfaebf14 100644 --- a/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types +++ b/tsc/testdata/baselines/reference/compiler/objectBindingPatternDefaultMissingElements.types @@ -96,6 +96,24 @@ export const typedArray = ([ ] = []) => {}; >[] : [] +export const typedObjectRest = ({ +>typedObjectRest : ({ required, ...rest }?: { required?: any; }) => void +>({ required, ...rest} = {}) => { rest;} : ({ required, ...rest }?: { required?: any; }) => void + + required, +>required : any + + ...rest +>rest : {} + +} = {}) => { +>{} : {} + + rest; +>rest : {} + +}; + uncheckedObject({ required: "value" }); >uncheckedObject({ required: "value" }) : void >uncheckedObject : ({ required, optional, }?: { optional?: boolean | undefined; required?: any; }) => void diff --git a/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts b/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts index 8586a95216b20..e0ea93281bfc9 100644 --- a/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts +++ b/tsc/testdata/tests/cases/compiler/objectBindingPatternDefaultMissingElements.ts @@ -40,6 +40,13 @@ export const typedArray = ([ optional = false, ] = []) => {}; +export const typedObjectRest = ({ + required, + ...rest +} = {}) => { + rest; +}; + uncheckedObject({ required: "value" }); uncheckedArray(["value"]); checkedObject({ required: "value" });