mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Read optional elements of bindings that hold undefined as undefined
`a?.[i]` on a binding whose type admits undefined, such as a `string[] | undefined` parameter, threw "undefined is not representable in the target union" when the binding held undefined. Element access replaced an identifier receiver whose storage has an undefined arm with a checked narrow to its present arm before the optional chain ran, so the chain saw a receiver that is never nullish and lowered `?.[` as a plain element read. Element access now leaves the receiver to the optional chain when the access carries `?.`; the chain tests the undefined arm and lowers the access again inside its guard. This applies to arrays, tuples and record keys. Property access (`a?.length`, `a?.at(0)`) already lowered through the chain. Corpus 8330 matches Node's output.
This commit is contained in:
@@ -13145,6 +13145,10 @@ export class Lowerer {
|
||||
) {
|
||||
return lowerElementAccess(this, expr);
|
||||
}
|
||||
// `a?.[i]`: the optional chain tests the receiver's undefined arm and
|
||||
// lowers this access again inside its guard.
|
||||
if (expr.questionDotToken && !this.chainHandled.has(expr))
|
||||
return lowerElementAccess(this, expr);
|
||||
const receiver = this.runtimeOptionalIdentifierValue(expr.expression);
|
||||
const receiverValue: IrExpr | null =
|
||||
receiver && (receiver.present.kind === "array" || receiver.present.kind === "record")
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8330-optional-element-undefined-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Optional element access on bindings whose type admits undefined: an
|
||||
// undefined receiver short-circuits to undefined without evaluating the
|
||||
// key, and a present receiver reads the element as a plain access does.
|
||||
|
||||
function first(kinds: string[] | undefined, i: number): string {
|
||||
const kind = kinds?.[i];
|
||||
return kind === undefined ? "none" : String(kind);
|
||||
}
|
||||
console.log(first(undefined, 0));
|
||||
console.log(first(["a", "b"], 1));
|
||||
console.log(first(["a"], 5));
|
||||
|
||||
function orDefault(values: number[] | undefined, i: number): number {
|
||||
return values?.[i] ?? -1;
|
||||
}
|
||||
console.log(orDefault(undefined, 0), orDefault([4, 5], 1), orDefault([4], 3));
|
||||
|
||||
function isMissing(flags: boolean[] | undefined, i: number): boolean {
|
||||
return flags?.[i] === undefined;
|
||||
}
|
||||
console.log(isMissing(undefined, 0), isMissing([true], 0), isMissing([false], 2));
|
||||
|
||||
function head(pair: [string, number] | undefined): string {
|
||||
return pair?.[0] ?? "empty";
|
||||
}
|
||||
console.log(head(undefined), head(["x", 1]));
|
||||
|
||||
function lookup(table: Record<string, number> | undefined, key: string): number | undefined {
|
||||
return table?.[key];
|
||||
}
|
||||
console.log(lookup(undefined, "a"), lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b"));
|
||||
|
||||
function width(words: string[] | undefined, i: number): number {
|
||||
return words?.[i]?.length ?? 0;
|
||||
}
|
||||
console.log(width(undefined, 0), width(["abc"], 0), width(["abc"], 1));
|
||||
|
||||
// The key is not evaluated when the receiver is undefined.
|
||||
let keys = 0;
|
||||
function nextKey(): number {
|
||||
keys++;
|
||||
return 0;
|
||||
}
|
||||
function counted(items: string[] | undefined): string | undefined {
|
||||
return items?.[nextKey()];
|
||||
}
|
||||
console.log(counted(undefined), keys);
|
||||
console.log(counted(["k"]), keys);
|
||||
|
||||
// Local bindings, including one reassigned between reads.
|
||||
function maybe(present: boolean): number[] | undefined {
|
||||
return present ? [7, 8] : undefined;
|
||||
}
|
||||
let current = maybe(false);
|
||||
console.log(current?.[0]);
|
||||
current = maybe(true);
|
||||
console.log(current?.[1]);
|
||||
current = maybe(false);
|
||||
console.log(current?.[1]);
|
||||
|
||||
function local(present: boolean): string {
|
||||
const items: string[] | undefined = present ? ["p", "q"] : undefined;
|
||||
const second = items?.[1];
|
||||
return second ?? "absent";
|
||||
}
|
||||
console.log(local(true), local(false));
|
||||
|
||||
// A sparse array reads its holes as undefined.
|
||||
const sparse: number[] = [1];
|
||||
sparse[3] = 4;
|
||||
function at(values: number[] | undefined, i: number): string {
|
||||
const value = values?.[i];
|
||||
return value === undefined ? "hole" : String(value);
|
||||
}
|
||||
console.log(at(sparse, 1), at(sparse, 3), at(undefined, 3));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2108 }
|
||||
Reference in New Issue
Block a user