Skip to Content
Course content

184: Record<Keys, Type> In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when I'm reviewing PRs from developers moving into TypeScript: they treat Record<K, T> as if it's just a stylistic preference—a "shorter way" to write an index signature. They'll write Record<string, number> thinking they've done something sophisticated, when in reality, they've just written { [key: string]: number } in a different syntax.

The misconception is that Record is a tool for flexibility. In truth, the real power of Record is constraint.

The Myth: Record is just a shorthand for Index Signatures

If you use string or number as your key type, Record doesn't actually give you any extra safety. Look at this:

type Inventory = Record<string, number>;

const stock: Inventory = {
  apples: 10,
  oranges: 20
};

// TypeScript is perfectly happy with this, but it's essentially a wild west.
// Any string is a valid key.
console.log(stock.bananas); // undefined, but no compiler error.

When you use string, you're telling TypeScript, "I have no idea what the keys will be, just that they'll be strings." That's fine for a generic cache or a dictionary, but it's not where Record earns its keep. If this is all you're doing, you're missing the point of the utility type.

The Reality: Enforcing Exhaustive Mappings

Where Record becomes a superpower is when you pair it with a Union Type. This allows you to tell TypeScript, "This object must have exactly these keys—no more, no less."

Imagine we're building a permission system for a CMS. We have a set of roles, and every single role must have a defined set of permissions. I don't want to accidentally forget to define permissions for a new role I just added to the system.

type UserRole = 'admin' | 'editor' | 'viewer';

interface Permissions {
  canEdit: boolean;
  canDelete: boolean;
  canPublish: boolean;
}

// This is the magic part. 
// We aren't just saying "keys are strings"; we're saying "keys must be UserRole".
const RolePermissions: Record<UserRole, Permissions> = {
  admin: { canEdit: true, canDelete: true, canPublish: true },
  editor: { canEdit: true, canDelete: false, canPublish: true },
  // Oops! I forgot 'viewer'. 
};

If you try to compile the code above, TypeScript will throw an error: Property 'viewer' is missing in type... but required in type 'Record<UserRole, Permissions>'. This is massive. I've had countless bugs in my career where a new enum value was added to a codebase, but the corresponding configuration object wasn't updated. Record kills that entire class of bug.

Combining Record with Keyof for Dynamic Constraints

Sometimes you don't want to hardcode a union. Instead, you want a record that maps to the keys of another existing type. This is a pattern I use constantly when creating "lookup" tables or strategy patterns.

interface Product {
  id: string;
  price: number;
  name: string;
}

// I want a way to format each property of the Product object.
// Instead of writing a custom type, I use keyof Product.
const ProductFormatters: Record<keyof Product, (val: any) => string> = {
  id: (val) => `ID: ${val}`,
  price: (val) => `$${val.toFixed(2)}`,
  name: (val) => val.toUpperCase(),
};

By using keyof Product, the ProductFormatters object is now tethered to the Product interface. If I add a description field to the Product interface tomorrow, TypeScript will immediately scream at me that ProductFormatters is missing the description handler. It turns your type system into a TODO list.




📋 Practical Task

Exercise: Build an HTTP Status Category Mapper

You are building a monitoring tool that groups HTTP status codes into categories. You need to ensure that every single "critical" status code is accounted for in your mapping object so that no error goes uncategorized.

Requirements:

  • Create a union type called CriticalStatusCode containing the following numbers: 400, 401, 403, 404, 500, 503.
  • Create a type called StatusCategory which is a union of 'Client Error' and 'Server Error'.
  • Implement a constant called StatusCodeMap using Record that maps every CriticalStatusCode to a StatusCategory.
  • Ensure the StatusCodeMap is exhaustive (if you miss one of the status codes, the compiler should complain).
// Your code here
Rating
0 0

There are no comments for now.

to be the first to leave a comment.