Files
scriptc/tests/corpus/record-nullish-empty-destructure.ts
T
Malte UblandClaude Opus 5.5 5a6f2c0902 Destructure options ?? {} over records with required members
`const { a, ...rest } = options ?? {}` destructures the options record or
an empty object. Where the record had a required member, the `{}`
fallback was coerced into the record and failed with SC2002. The source
now lowers to the union of the record and the empty record, so the
union pattern lowering reads each key from the arm that holds it, the
empty arm answers undefined and the rest element packs nothing. A
record arm without a literal key now answers undefined in unionKeyGet
too, which the frontend already asked for and the validator rejected
as an internal error. All-optional options keep their lowering.

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

84 lines
2.8 KiB
TypeScript

// `const { a, ...rest } = options ?? {}` (and `|| {}`): the pattern
// destructures either the options record or an empty object. Each key reads
// undefined from the empty object, a default applies to it, and the rest
// element packs the record's remaining own keys or nothing. The options
// expression is evaluated once.
interface EditorOptions {
description: string;
paddingX: number;
label?: string;
}
function editor(options?: EditorOptions): string {
const { description, ...editorOptions } = options ?? {};
return `${description} ${JSON.stringify(editorOptions)} [${Object.keys(editorOptions).join(",")}]`;
}
console.log(editor());
console.log(editor({ description: "d", paddingX: 2 }));
console.log(editor({ description: "d", paddingX: 2, label: "l" }));
// Every member optional: the empty object is the same layout with every
// key absent.
interface StreamOptions {
apiKey?: string;
env?: string;
signal?: string;
transformHeaders?: (headers: string) => string;
}
function stream(options?: StreamOptions): string {
const { transformHeaders, apiKey, ...rest } = options ?? {};
const headers = transformHeaders ? transformHeaders("h") : "none";
return `${headers} ${apiKey} ${JSON.stringify(rest)} ${"signal" in rest}`;
}
console.log(stream());
console.log(stream({ apiKey: "k", signal: "s" }));
console.log(stream({ transformHeaders: (h) => h + "!", env: "e" }));
// A nested optional record, renamed keys and a default.
interface Entry {
options?: { deferred: boolean; signal?: string; step: number };
}
function submit(entry: Entry): string {
const { deferred: _deferred, signal: _signal, step = -1, ...submission } = entry.options ?? {};
return `${step} ${JSON.stringify(submission)}`;
}
console.log(submit({}));
console.log(submit({ options: { deferred: true, step: 4 } }));
console.log(submit({ options: { deferred: false, signal: "x", step: 0 } }));
// `||` and a null arm.
interface Retry {
headers: string;
retries: number;
}
function retry(value: Retry | null): string {
const { headers, retries = 3 } = value || {};
return `${headers} ${retries} ${typeof retries}`;
}
console.log(retry(null));
console.log(retry({ headers: "h", retries: 2 }));
// A generic options parameter.
function generic<TOptions extends { apiKey?: string; maxTokens: number }>(options?: TOptions): string {
const { apiKey, ...providerOptions } = options ?? {};
return `${apiKey} ${JSON.stringify(providerOptions)}`;
}
console.log(generic());
console.log(generic({ maxTokens: 3, apiKey: "k" }));
// The options expression runs once.
let calls = 0;
function current(): EditorOptions | undefined {
calls++;
return calls === 1 ? undefined : { description: "second", paddingX: 1 };
}
const { description: first } = current() ?? {};
const { description: second, paddingX } = current() ?? {};
console.log(first, second, paddingX, calls);
export {};