How can I iterate over Record keys in a proper type-safe way?

I think the right way to do this is to create an immutable array of the key names and give it a narrow type so that the compiler recognizes it as containing string literal types instead of just string. This is easiest with a const assertion:

const humanProps = ["weight", "height", "age"] as const;
// const humanProps: readonly ["weight", "height", "age"]

Then you can define HumanProp in terms of it:

type HumanProp = typeof humanProps[number];

And the rest of your code should work more or less as-is, except that when you iterate over keys you should use your immutable array above instead of Object.keys():

type Human = Record<HumanProp, number>;

const alice: Human = {
   age: 31,
   height: 176,
   weight: 47
};

const humanPropLabels: Readonly<Record<HumanProp, string>> = {
   weight: "Weight (kg)",
   height: "Height (cm)",
   age: "Age (full years)"
};

function describe(human: Human): string {
    let lines: string[] = [];
    for (const key of humanProps) { // <-- iterate this way
        lines.push(`${humanPropLabels[key]}: ${human[key]}`);
    }
    return lines.join("\n");
}

The reason not to use Object.keys() is that the compiler can’t verify that an object of type Human will only have the keys declared in Human. Object types in TypeScript are open/extendible, not closed/exact. This allows interface extension and class inheritance to work:

interface SuperHero extends Human {
   powers: string[];
}
declare const captainStupendous: SuperHero;
describe(captainStupendous); // works, a SuperHero is a Human

You wouldn’t want describe() to explode because you pass in a SuperHero, a special type of Human with an extra powers property. So instead of using Object.keys() which correctly produces string[], you should use the hardcoded list of known properties so that code like describe() will ignore any extra properties if they are present.


And, if you add an element to humanProps, you’ll see errors where you want while describe() will be unchanged:

const humanProps = ["weight", "height", "age", "shoeSize"] as const; // added prop

const alice: Human = { // error! 
   age: 31,
   height: 176,
   weight: 47
};

const humanPropLabels: Readonly<Record<HumanProp, string>> = { // error!
   weight: "Weight (kg)",
   height: "Height (cm)",
   age: "Age (full years)"
};

function describe(human: Human): string { // okay
   let lines: string[] = [];
   for (const key of humanProps) {
      lines.push(`${humanPropLabels[key]}: ${human[key]}`);
   }
   return lines.join("\n");
}

Okay, hope that helps; good luck!

Playground link to code

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)