Memoize an empty literal's type separately as a union arm

The type mapper maps the type inferred from an empty object literal to the
empty record as an arm of an unreduced union and to a checked-dynamic
value elsewhere, but both answers shared one memo entry. Once the type had
been mapped on its own, a union such as `typeof empty | DataView` reused
the dynamic answer for its arm and refused (SC2009). The memo key now
includes whether the mapping is for such an arm.
This commit is contained in:
Chris Tate
2026-10-21 21:28:27 -05:00
parent d15e822d26
commit 4a5ddadd3d
2 changed files with 12 additions and 1 deletions
@@ -1655,7 +1655,10 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const topLevel = mapTypeDepth === 0;
const typeId = (type as { id?: number }).id;
const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true);
const memoKey = memoizable ? `${0}:${typeId}` : null;
// An empty literal's type maps to the empty record as an unreduced union's
// arm (emptyLiteralArm) and to the checked-dynamic value elsewhere: the two
// answers keep separate memo entries.
const memoKey = memoizable ? `${ctx.emptyLiteralArm ? 1 : 0}:${typeId}` : null;
if (memoKey !== null) {
const memo = ctx.typeMemo?.get(memoKey);
if (memo !== undefined) return memo;
@@ -30,3 +30,11 @@ console.log(typeof choose(1), typeof choose(-1));
const empty = {};
console.log(JSON.stringify(empty), Object.keys(empty).length, empty === empty, String(empty));
// An empty literal's type beside a typed arm of an unreduced union, after
// the type was already used on its own.
const viewed = {};
console.log(JSON.stringify(viewed));
const views: (typeof viewed | DataView)[] = [viewed, new DataView(new ArrayBuffer(2))];
for (const view of views)
console.log(view instanceof DataView ? `view ${view.byteLength}` : JSON.stringify(view));