Skip to content

typescript: a name destructured from a type-literal-typed value takes its member's type - #2011

Open
swapnilpaliwal-sd wants to merge 2 commits into
0.2.3from
fix/ts-destructure-from-shape
Open

swapnilpaliwal-sd wants to merge 2 commits into
0.2.3from
fix/ts-destructure-from-shape

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Symptom

A name destructured out of a value whose declared type is an object type literal stays untyped, so every call on it is ambiguous_unknown, while the member access beside it resolves:

function f(opts: { port: Port }) {
  opts.port.execute(1);          // known_edge
  const { port } = opts;
  port.execute(1);               // ambiguous_unknown (before)
}
type Opts = { port: Port };
function g({ port }: Opts) { port.execute(2); }   // ambiguous_unknown (before)

Cause

The destructuring rules (binding_pattern_type, var_binding_target) read the source's type (param_type / expr_type). A type literal has no declaration to be a type. It is a shape (param_shape_target / expr_shape), and nothing on the destructuring side read shapes.

Fix

reference-types.dl gets a shape twin of each rule, for parameters (binding_pattern_shape) and variables (var_binding_shape), covering field types, nested shapes and methods. A shape is its own scope (scope_sibling(s, s)), so the existing declared_field / declared_method lookups are reused. No negation is added. param_type gets one more positive clause, built only from rows that sit below the expression fixpoint.

Tests

  • New case 100-destructure-from-type-literal (oracle-checked). It covers inline and aliased literals, parameters and variables, a nested shape, and a shape method. Controls: an any source stays unresolved; a look-alike literal { execute(x) } resolves to its own method, not to Port.execute.
  • Case 75-jsx-wrapper-guards: five destructured-prop calls now resolve to exactly the targets the compiler oracle had listed as known-missing, and the shadowing guard still holds. Its debt list drops from 6 entries to 1.
  • Second commit, test tool: normalize_edges.py labelled a destructured parameter once per bound name (aliasParam(Opts,Opts,Opts); the compiler prints aliasParam(Opts)), so a right edge showed up as one missing plus one extra. It now keeps one row per position. Case 100 needs no known-missing file.
  • Full TypeScript suite: 110/110.

Note for #2010: its case 99 lists three edges as known-missing only because of that label. Once both are in, those lines start passing and must be removed (re-bless 99 with --oracle).

Real project (1.9k-file Deno TypeScript repo)

base 0.2.3 this PR
known_edge 7,410 7,441
ambiguous_unknown 35,820 35,789
resolved edges lost 0
index, warm 40.2 s 36.1 s

Spot-checked new edges: an interface method reached through const { transport } = opts with opts: { transport: Transport; ... }, and methods of an inline utils: { json: { omit(..) } } parameter type.

Stacks cleanly with #2010 (contextual destructured parameters); the two touch different rules.

… its member's type

`const { port } = opts` with `opts: { port: Port }`, and `({ port }: Opts)` with
`type Opts = { port: Port }`, left `port` untyped, so `port.execute()` was
unresolved while `opts.port.execute()` beside it resolved. A type literal has no
declaration for param_type / expr_type to name: it is a shape. The destructuring
rules for parameters and variables now read the shape too (a shape is its own
scope), for fields, nested shapes and methods.

Case 100 covers inline and aliased literals, parameters and variables, a nested
shape and a shape method, with controls: an untyped source stays unresolved, and
a look-alike literal resolves to its own method, not to the interface. Case 75:
five destructured-prop calls now resolve to the targets the compiler oracle had
listed as known-missing (the shadowing guard still holds); they still show as
missing+extra only because a destructured caller is labelled once per bound name.

On a 1.9k-file Deno TypeScript repo: known_edge 7,410 -> 7,441, ambiguous_unknown
35,820 -> 35,789, 0 resolved edges lost; spot-checked against the source.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
The parser emits a row for a destructuring pattern and one per name it binds, all at
the pattern's position. normalize_edges.py turned each into a parameter, so
`aliasParam({ port, clock }: Opts)` was labelled `aliasParam(Opts,Opts,Opts)` while the
compiler oracle prints `aliasParam(Opts)`: the same edge showed as one missing and
one extra. One row per position, the pattern's when present.

Case 100's three known-missing edges and five of case 75's now agree with the oracle;
both debt lists are trimmed. Suite 110/110.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant