Read records by keyof keys when their fields differ in type (#1883)

This commit is contained in:
Chris Tate
2026-10-22 18:54:16 -05:00
committed by GitHub
4 changed files with 57 additions and 0 deletions
@@ -1321,6 +1321,15 @@ export function lowerRecordKeyRead(
shape.fields.every((f) => typeEquals(f.type, shape.fields[0]!.type))
) {
declared = shape.fields[0]!.type;
} else if (shape.fields.length > 0) {
// Fields of different types (`row[key]` with `key: keyof Row`): the
// checker types the read as the union of the field types the key can
// name. When every declared field surfaces in that union (an arm, or a
// union that widens into it), the per-field dispatch wraps each hit
// into its arm.
const read = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (read !== null && read.kind === "union" && recordKeyResultOk(lowerer, shape, read))
declared = read;
}
// Fields of different types answer a checked-dynamic value where nothing
// typed the read: in JavaScript, and where the checker types it 'any'
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7400-record-keyof-union-read.ts"
],
"diags": []
}
@@ -0,0 +1,41 @@
// Keyed reads of records whose fields have different types, with a key
// typed as `keyof` the record: the read is the union of the field types.
interface Book {
readonly id: string;
readonly title: string;
readonly year: number;
readonly tags: readonly string[];
readonly available: boolean;
}
type BookPatch = Partial<Omit<Book, "id">>;
function changedKeys(current: Book, patch: BookPatch): string[] {
const changed: string[] = [];
for (const key of Object.keys(patch) as (keyof BookPatch)[]) {
const before = current[key];
const after = patch[key];
if (after === undefined) continue;
if (Array.isArray(before) && Array.isArray(after)) {
if (before.join("|") !== after.join("|")) changed.push(key);
} else if (before !== after) changed.push(key);
}
return changed;
}
function describe(book: Book, key: keyof Book): string {
const value = book[key];
if (typeof value === "string") return `${key}: string ${value}`;
if (typeof value === "number") return `${key}: number ${value.toFixed(1)}`;
if (typeof value === "boolean") return `${key}: boolean ${value ? "yes" : "no"}`;
return `${key}: list of ${value.length} [${value.join(", ")}]`;
}
const book: Book = { id: "b1", title: "Atlas", year: 1999, tags: ["maps", "travel"], available: true };
console.log(changedKeys(book, { title: "Atlas", year: 2001 }));
console.log(changedKeys(book, { tags: ["maps", "travel"], available: false }));
console.log(changedKeys(book, { tags: ["maps"], title: "Atlas II", year: undefined }));
for (const key of ["id", "title", "year", "tags", "available"] as const) console.log(describe(book, key));
const keys: (keyof Book)[] = ["year", "tags"];
const values = keys.map((k) => book[k]);
console.log(values, values.length);
console.log(JSON.stringify(Object.fromEntries(keys.map((k) => [k, book[k]]))));
@@ -0,0 +1 @@
{ "plain": 2600, "san": 20500 }