Files
scriptc/tests/corpus/nullish-empty-array-default.ts
T
Malte UblandClaude Opus 5.5 677710075a Let an empty ?? array default adopt the left's array arm
`''.match(re) ?? []` typed the literal uninhabited (`never[]`), whose element
still mapped through never's f64 value representation, so the whole
expression was `RegExpMatchArray | number[]` — a result no single array slot
can receive. A union return such as `"all" | readonly string[]` refused with
SC2003 "union conversion has no unambiguous layout mapping" (`getScriptReach`
in a coding agent's tool-discovery path).

An EMPTY array-literal default has no element of its own: it adopts the
left's present array arm, exactly the rule the union slot and the ternary arm
already state for an empty literal. The left's nullish test and the lazy
default are unchanged, and a default whose own type already matches the left
keeps the identical lowering it had.

Corpus: tests/corpus/nullish-empty-array-default.ts pins the union return,
string element reads through the default, the declared optional-array slot
with a fresh empty value per evaluation, and a parenthesized/template default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:10:46 -07:00

41 lines
1.8 KiB
TypeScript

// `''.match(re) ?? []`: the empty array-literal default of a `??` adopts the
// left's present array arm. tsc gives the literal the uninhabited `never[]`
// element, which the compiler maps through never's f64 value representation
// (`number[]`), so the whole expression used to be `RegExpMatchArray |
// number[]` — a result no single array slot can receive.
// A union return (`"all" | readonly string[]`) whose array arm is the
// `?? []` result.
function reach(code: unknown): "all" | readonly string[] {
if (typeof code !== "string") return [];
return /\bALL\b/.test(code) ? "all" : (code.match(/\w+/g) ?? []);
}
console.log(JSON.stringify(reach(7)));
console.log(JSON.stringify(reach("ALL")));
console.log(JSON.stringify(reach("a.b c")));
console.log(JSON.stringify(reach("")));
// The default's element type is the left's: string element reads and a
// map callback over the value compile as strings, and an absent match
// answers the empty array Node does.
const names = "a-b c".match(/[a-z]+/g) ?? [];
console.log(names.map((name) => name.toUpperCase()).join("+"));
const none = "123".match(/[a-z]+/g) ?? [];
console.log(none.length, JSON.stringify(none), names.length);
// A declared optional array keeps its own type through the same default,
// and the fresh empty array is a value of its own (mutation, identity).
function fill(source: number[] | undefined, with_: number): number[] {
const list = source ?? [];
list.push(with_);
return list;
}
console.log(JSON.stringify(fill(undefined, 1)), JSON.stringify(fill([7, 8], 9)));
const first = fill(undefined, 2);
const second = fill(undefined, 3);
console.log(first !== second, JSON.stringify(first), JSON.stringify(second));
// A parenthesized default and one inside a longer expression.
const longest = ("abbbc".match(/b+/g) ?? []).length;
console.log(longest, `${JSON.stringify("xyz".match(/q/g) ?? [])}!`);