mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -4948,6 +4948,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const record = mapRecordType(widened, ctx);
|
||||
if (record) return record;
|
||||
}
|
||||
// An intersection whose constituents all map to ONE representation is
|
||||
// that representation. An intersection that restates an optional member
|
||||
// as required (`Opts & { files: Array<{ path: string }> }` over
|
||||
// `files?: Array<{ path: string }>`) gives the member the type `A[] & B[]`
|
||||
// with two separately written but structurally equal element literals;
|
||||
// the checker keeps both, while their arrays (tuples, maps, ...) intern
|
||||
// to one IR type. Reached only after every other intersection rule
|
||||
// declined, so intersections that already mapped keep their mapping. A
|
||||
// dynamic result stays unmapped: two opaque types are not one type.
|
||||
if (widened.isIntersectionType()) {
|
||||
const same = mapEqualConstituentsIntersection(widened, ctx);
|
||||
if (same) return same;
|
||||
}
|
||||
// Unions. `boolean` is internally `true | false`, and narrowing produces
|
||||
// literal unions like `"a" | "b"` — parts that map to the SAME IR type
|
||||
// collapse (deduplicated by typeKey), so a single surviving arm is just
|
||||
@@ -6813,6 +6826,19 @@ export function withUnitArm(
|
||||
* assigned keys, which is also Node's answer for Object.assign(fn, obj)
|
||||
* (a function's own length/name are non-enumerable). Null whenever the
|
||||
* shape doesn't fit — the caller falls through to the plain-func mapping. */
|
||||
/** The IR type every constituent of an intersection maps to, or null when a
|
||||
* constituent is unmappable or dynamic, or two constituents differ. */
|
||||
function mapEqualConstituentsIntersection(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
let same: IrType | null = null;
|
||||
for (const part of ts.constituentTypes(widened)) {
|
||||
const mapped = mapType(part, ctx);
|
||||
if (mapped === null || mapped.kind === "dyn") return null;
|
||||
if (same === null) same = mapped;
|
||||
else if (!typeEquals(same, mapped)) return null;
|
||||
}
|
||||
return same;
|
||||
}
|
||||
|
||||
function mapHybridCallableIntersection(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const { checker, shapes } = ctx;
|
||||
if (!widened.isIntersectionType()) return null;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/intersection-restated-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// An intersection that restates a member with a separately written but
|
||||
// structurally equal type maps as that one type. Restating an optional member
|
||||
// as required makes it required; the checker keeps both element literals
|
||||
// (`A[] & B[]`), and their arrays, tuples, and maps share one representation.
|
||||
|
||||
interface Options {
|
||||
root: string;
|
||||
files?: Array<{ path: string; size: number }>;
|
||||
}
|
||||
type Normalized = Options & { files: Array<{ path: string; size: number }> };
|
||||
|
||||
function normalize(options: Options): Normalized {
|
||||
return { ...options, files: options.files ?? [] };
|
||||
}
|
||||
|
||||
function totalSize(n: Normalized): number {
|
||||
let total = 0;
|
||||
for (const f of n.files) total += f.size;
|
||||
return total;
|
||||
}
|
||||
|
||||
const plain = normalize({ root: "/a" });
|
||||
console.log(plain.root, plain.files.length, totalSize(plain));
|
||||
const full = normalize({
|
||||
root: "/b",
|
||||
files: [
|
||||
{ path: "x.txt", size: 3 },
|
||||
{ path: "y.txt", size: 4 },
|
||||
],
|
||||
});
|
||||
full.files.push({ path: "z.txt", size: 5 });
|
||||
console.log(full.files.map((f) => f.path).join(","), totalSize(full));
|
||||
console.log(JSON.stringify(full));
|
||||
|
||||
let maybe: Normalized | undefined;
|
||||
if (process.argv.length < 99) maybe = { root: "/", files: [{ path: "a", size: 1 }] };
|
||||
console.log(maybe?.root, maybe?.files[0]?.path);
|
||||
|
||||
// Nested records whose inner arrays are restated.
|
||||
interface Settings {
|
||||
name: string;
|
||||
meta?: { tags: Array<{ key: string; value: string }> };
|
||||
}
|
||||
type Resolved = Settings & { meta: { tags: Array<{ key: string; value: string }> } };
|
||||
const resolved: Resolved = { name: "s", meta: { tags: [{ key: "k", value: "v" }] } };
|
||||
resolved.meta.tags.push({ key: "k2", value: "v2" });
|
||||
for (const t of resolved.meta.tags) console.log(`${t.key}=${t.value}`);
|
||||
|
||||
// Tuples, nested arrays, and maps.
|
||||
interface Layout {
|
||||
pair?: [{ x: number }, string];
|
||||
grid?: Array<Array<{ cell: number }>>;
|
||||
index?: Map<string, { hits: number }>;
|
||||
}
|
||||
type Filled = Layout & {
|
||||
pair: [{ x: number }, string];
|
||||
grid: Array<Array<{ cell: number }>>;
|
||||
index: Map<string, { hits: number }>;
|
||||
};
|
||||
const filled: Filled = {
|
||||
pair: [{ x: 7 }, "seven"],
|
||||
grid: [[{ cell: 1 }, { cell: 2 }], [{ cell: 3 }]],
|
||||
index: new Map([["home", { hits: 2 }]]),
|
||||
};
|
||||
filled.index.set("about", { hits: 1 });
|
||||
console.log(filled.pair[0].x, filled.pair[1]);
|
||||
console.log(filled.grid.map((row) => row.map((c) => c.cell).join("+")).join(" | "));
|
||||
for (const [k, v] of filled.index) console.log(k, v.hits);
|
||||
|
||||
// Both sides required, and both sides optional (the member stays optional).
|
||||
type Twice = { items: Array<{ n: number }> } & { items: Array<{ n: number }> };
|
||||
const twice: Twice = { items: [{ n: 1 }, { n: 2 }] };
|
||||
console.log(twice.items.reduce((s, i) => s + i.n, 0));
|
||||
type BothOptional = { extra?: Array<{ n: number }> } & { extra?: Array<{ n: number }> };
|
||||
const none: BothOptional = {};
|
||||
const some: BothOptional = { extra: [{ n: 9 }] };
|
||||
console.log(none.extra === undefined, some.extra?.[0]?.n);
|
||||
|
||||
// A restated member read through a function value.
|
||||
type Handler = (event: Normalized) => string;
|
||||
const describe: Handler = (event) => `${event.root}:${event.files.length}`;
|
||||
console.log(describe(full), describe(plain));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000 }
|
||||
Reference in New Issue
Block a user