Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/compiler/src/frontend/type-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5139,11 +5139,15 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
const obj = type.getObjectType();
if (obj === undefined || !(obj.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(obj);
const idx = type.getIndexType();
if (idx === undefined) return null;
// `T[keyof T]` over a T bound to an unknown-valued record view
// (`Record<string, unknown>` maps to the checked-dynamic value, see
// mapRecordType): every member reads as unknown.
if (bound?.kind === "dyn" && idx.isIndexType() && idx.getTarget() === obj) return DYN;
if (!bound || bound.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const idx = type.getIndexType();
if (idx === undefined) return null;
let covered: IrType[] | null = null;
if (idx.isStringLiteralType()) {
const f = shape.fields.find((x) => x.name === idx.value);
Expand Down Expand Up @@ -5199,6 +5203,17 @@ function mapBoundKeyofNeverConditional(type: ts.Type, ctx: TypeMapperCtx): IrTyp
const target = check.getTarget();
if (!target || !(target.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(target);
// A T bound to the checked-dynamic value (`Record<string, unknown>`, an
// unknown-valued record view) has no static key set to decide the
// branch, but both branches of the lib's `keyof T extends never ?
// unknown : T[keyof T]` read as unknown there: one answer either way.
if (bound?.kind === "dyn") {
const whenEmpty = type.getTrueType();
const otherwise = type.getFalseType();
const a = whenEmpty ? mapType(whenEmpty, ctx) : null;
const b = otherwise ? mapType(otherwise, ctx) : null;
return a && b && typeEquals(a, b) ? a : null;
}
if (bound?.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6468-generic-unknown-record-entries.ts"
],
"diags": []
}
33 changes: 33 additions & 0 deletions tests/corpus/6468-generic-unknown-record-entries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Inside a generic body, `Object.entries(tags)` over `T extends
// Record<string, unknown>` reads `[string, T[keyof T]]` elements. An
// instance whose T is the unknown-valued record view reads every member
// as unknown; a record instance reads the union of its member types.
function upper(text: string): string {
return text.toUpperCase();
}

function scrub(text: string): string {
return text.replace(/secret/g, "***");
}

function describe<T extends Record<string, unknown>>(tags: T): string[] {
return Object.entries(tags).map(
([key, value]) => `${key}=${typeof value === "string" ? upper(value) : String(value)}`,
);
}

function redact<T extends Record<string, unknown>>(tags: T): T {
return Object.fromEntries(
Object.entries(tags).map(([key, value]) => [key, typeof value === "string" ? scrub(value) : value]),
) as T;
}

function flatten(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : { value };
}

console.log(describe(flatten({ b: 1, a: "x", c: true })));
console.log(describe(flatten(5)));
console.log(describe({ name: "n", size: 3 }));
const cleaned = redact(flatten({ token: "secret", count: 2 }));
console.log(cleaned, Object.keys(cleaned));
Loading