Files
scriptc/tests/corpus/1549-array-isarray-unions.ts
Chris TateandJohn Lindquist 1e4f71dddf fix(compiler): recognize tuples in Array.isArray (#154)
* fix(compiler): recognize tuples in Array.isArray

- Treat fixed tuple record shapes as JavaScript arrays.
- Keep union tag tests and narrowing aligned.
- Add Native SDK facade regression coverage.

Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com>

* fix(compiler): narrow readonly tuple arrays

* fix(compiler): preserve narrowed readonly tuples

---------

Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com>
2026-08-14 10:50:35 -05:00

110 lines
4.3 KiB
TypeScript

// Array.isArray on UNION-typed values answers by the runtime tag: true iff
// the active arm is an array kind — the narrowing test tsc's control flow
// then builds on (the certs `string | readonly string[]` and tailscale
// `string[] | undefined` idioms).
function names(tlds: string | string[]): string {
const configured: string[] = Array.isArray(tlds) ? tlds : [tlds];
return configured.join(",");
}
console.log(names("localhost"));
console.log(names(["a.test", "b.test"]));
console.log(names([]));
// readonly arrays ride the same tag test (tsc's own narrowing answers
// `any[]` around an `arg is any[]` guard on readonly unions, so the
// branches read nothing here — the certs SNICallback idiom's shape).
function isList(tlds: string | readonly string[]): boolean {
return Array.isArray(tlds);
}
console.log(isList(["x.test", "y.test"]));
console.log(isList("bare"));
function hasHttps(capabilities: string[] | undefined): boolean {
if (Array.isArray(capabilities) && capabilities.some((c) => c === "https")) {
return true;
}
return false;
}
console.log(hasHttps(undefined));
console.log(hasHttps(["ssh", "https"]));
console.log(hasHttps(["ssh"]));
// The negated spelling narrows the else way around.
function total(value: number | number[]): number {
if (!Array.isArray(value)) return value;
let sum = 0;
for (const n of value) sum += n;
return sum;
}
console.log(total(7));
console.log(total([1, 2, 3]));
// Zero array arms fold to constant false; a union of two array kinds
// answers true for both.
const scalar: string | number = "text" as string | number;
console.log(Array.isArray(scalar));
const mixed: string[] | number[] | undefined = [3, 4] as string[] | number[] | undefined;
console.log(Array.isArray(mixed));
const missing: string[] | number[] | undefined = undefined;
console.log(Array.isArray(missing));
// Fixed tuples use a positional record-shaped IR representation so each
// slot can keep its own type, but they are still JavaScript arrays. This is
// the Native SDK facade shape: an update returns either its model record or
// [model, command], then Array.isArray selects the tuple before indexing it.
interface TupleModel {
n: number;
}
interface TupleCommand {
op: string;
}
function tupleUpdate(model: TupleModel, effect: boolean): TupleModel | [TupleModel, TupleCommand] {
if (!effect) return model;
return [model, { op: "spawn" }];
}
function normalizeTuple(model: TupleModel, effect: boolean): [TupleModel, string] {
const out = tupleUpdate(model, effect);
if (Array.isArray(out)) return [out[0], out[1].op];
return [out, "none"];
}
function isFixedTuple(value: [TupleModel, TupleCommand]): boolean {
return Array.isArray(value);
}
// For a readonly tuple union, tsc narrows the guarded value to an
// intersection with any[] rather than directly to an array type. The
// runtime tag still proves the tuple arm, so positional reads must bridge
// back to its fixed shape.
type ReadonlyTupleResult = TupleModel | readonly [TupleModel, TupleCommand];
function describeReadonlyTuple(value: ReadonlyTupleResult): string {
if (Array.isArray(value)) return `${value[0].n}:${value[1].op}`;
return "plain";
}
// The checker keeps the same synthetic intersection for neighboring tuple
// operations. A const alias must retain the lowered tuple representation;
// then length, the supported read-only methods, and iteration all dispatch
// from that representation too.
function summarizeReadonlyTuple(value: ReadonlyTupleResult): string {
if (!Array.isArray(value)) return "plain";
const tuple = value;
let visits = 0;
for (const _part of tuple) visits++;
return `${value.length}:${value.slice(0).length}:${value.map(() => "v").join("")}:${tuple.length}:${tuple.slice(0).length}:${tuple.map(() => "t").join("")}:${visits}`;
}
const plainResult = normalizeTuple({ n: 7 }, false);
console.log(plainResult[0].n, plainResult[1]);
const tupleResult = normalizeTuple({ n: 9 }, true);
console.log(tupleResult[0].n, tupleResult[1]);
console.log(isFixedTuple([{ n: 11 }, { op: "direct" }]));
const readonlyTuple: readonly [TupleModel, TupleCommand] = [{ n: 12 }, { op: "readonly" }];
console.log(describeReadonlyTuple({ n: 13 }));
console.log(describeReadonlyTuple(readonlyTuple));
console.log(summarizeReadonlyTuple({ n: 14 }));
console.log(summarizeReadonlyTuple(readonlyTuple));