diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 6305a6b63..c171c5c4b 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -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; diff --git a/tests/corpus/6909-empty-object-type-values.ts b/tests/corpus/6909-empty-object-type-values.ts index b539415fd..f845e19a6 100644 --- a/tests/corpus/6909-empty-object-type-values.ts +++ b/tests/corpus/6909-empty-object-type-values.ts @@ -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));