Files
scriptc/tests/diagnostics/destructuring-fences.ts
T
Malte UblandClaude Opus 5.5 a0a6910e4e Destructure union sources by narrowing first and joining arm reads
An object pattern over a union whose arms type a field differently, or do
not declare it at all, was refused with SC1031, even though the pattern
element's own type is the checker's answer: the join of the arms' declared
types, with undefined for an arm that omits the key, or the narrowed arm
when a discriminant test or an `in` guard refined the source at the
pattern's site.

Keep the unionDisc fast path wherever every arm declares the field with
one type, so programs that compile today emit identical IR. Otherwise read
through the existing keyed join (unionKeyGet): each arm answers its
declared type and a record arm that does not declare the key answers
undefined, which is exactly the desugar's own property read. Only
destructuring passes the new missingFieldAsUndefined flag, so dotted reads
keep their current refusals.

Narrow first when the un-narrowed pattern does not fit: bind the
checker-narrowed view of the source to a hidden local — a trusting
arm extraction for one arm, a sub-union re-tag whose excluded arms trap,
and the stored layout view for an intersection refinement — and lower the
pattern over that view.

A rest element over a union source packs each arm's unconsumed fields into
a fresh record of the checker's arm-wise rest type, selected by the
runtime tag; CopyDataProperties makes a fresh object too, so nothing
aliases the source. Shapes that do not spell the rest type still fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:37:58 -07:00

70 lines
2.5 KiB
TypeScript

// Destructuring now lowers defaults in every position (tuple, array with
// the bounds test, record fields, nested patterns, whole-pattern and
// rest-pattern parameters, getter results), rest in every declaration
// position (array slices, tuple tails pack fresh, object rest copies the
// unconsumed fields — class instances included, corpus 2539),
// assignment-position patterns over static sources — record AND
// class-instance, with nested patterns, property/element targets, and
// defaults (corpus 2536-2538) — and for-of expression heads. What stays
// fenced is the honest residue: METHOD extraction over class instances
// (a detached method loses its receiver), rest whose packed type is not
// a plain record (a class of methods only packs '{}' — 'unknown'),
// rest that would copy a NON-PUBLIC field (JS copies it; the rest type
// cannot name it), setter-only properties, union sources (narrow
// first), object patterns over arrays, index-signature rest packing,
// defaults on NESTED assignment targets (no single binding type to
// test against), and assignment targets with no static write form.
class C {
f = 1;
describe(): string {
return `f=${this.f}`;
}
set onlyIn(v: number) {
this.f = v;
}
}
const { describe } = new C(); // method extraction keeps the fence
console.log(describe());
const { f, ...restOfC } = new C(); // rest over a class instance keeps the fence
console.log(f, Object.keys(restOfC).length);
const { onlyIn } = new C(); // tsc types the setter-only read; the fence names it
console.log(onlyIn);
interface E1 {
kind: "one";
b: string;
count: number;
}
interface E2 {
kind: "two";
b: number;
label: string;
}
function fromUnion(u: E1 | E2): string {
const { b } = u; // arms type the field differently: the joined read compiles
return String(b);
}
console.log(fromUnion({ kind: "one", b: "s", count: 1 }));
const xs = [1, 2, 3];
const { push: append } = xs; // object patterns over arrays read positions and length only
console.log(typeof append);
const indexed: Record<string, number> = { a: 1 };
const { ...allOfIt } = indexed; // index-signature rest needs overflow packing
console.log(allOfIt["a"]);
const holder = { inner: { v: 1 } };
let sink = 0;
({ inner: { v: sink } = { v: 2 } } = holder); // a DEFAULT on a nested assignment target keeps the fence
console.log(sink);
class WithPrivate {
pub = 1;
private hidden = 2;
also = 3;
}
const { pub, ...restOfPriv } = new WithPrivate(); // JS copies 'hidden' into the rest object; the rest type cannot name it
console.log(pub, JSON.stringify(restOfPriv));