Skip to main content

TS Ep 35: Built-in Utility Types (`Record`)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 35: This Article
When you need an object that acts as a dictionary (mapping arbitrary strings or numbers to a specific value type), writing an inline index signature [key: string]: V is tedious. TypeScript’s built-in Record<K, V> utility constructs strict dictionary shapes instantly.

1. Simple Dictionaries (Record<string, V>)
#

When you don’t know the exact names of the keys in advance, but you know that every key must map to a specific value type, use Record<string, V>.

// Any string key is permitted, but the value MUST be a number.
const productPrices: Record<string, number> = {
  apple: 1.99,
  banana: 0.99,
  laptop: 1500,
};

// 🟢 Valid additions
productPrices.orange = 2.50;
productPrices["mechanical_keyboard"] = 120;

// ❌ Compiler Error: Type 'string' is not assignable to type 'number'.
// productPrices.watermelon = "three dollars";

Under the Hood Comparison
#

Record<string, number> is perfectly equivalent to writing a manual Index Signature, but it is vastly more readable:

// These two types are structurally identical:
type MapA = Record<string, number>;
type MapB = { [key: string]: number };

2. Exhaustive Mapping with Unions
#

The true power of Record<K, V> unlocks when K is constrained to a specific Union Type.

When you pass a Union as the Key parameter, TypeScript enforces that every single member of the union MUST exist as a key in the resulting object. This prevents missing configuration keys.

type Environment = "development" | "staging" | "production";

// 🟢 The object MUST implement ALL three environments!
const apiEndpoints: Record<Environment, string> = {
  development: "http://localhost:3000",
  staging: "https://staging.api.com",
  production: "https://api.com",
};

// ❌ Compiler Error: Property 'production' is missing in type '{ ... }'
/*
const badEndpoints: Record<Environment, string> = {
  development: "http://localhost:3000",
  staging: "https://staging.api.com",
};
*/

3. Combining Record with Other Utility Types
#

Because utility types are generic definitions, they compose beautifully. You can build advanced domain constraints by nesting Record, Partial, and Readonly.

Example: Role-Based Permission Matrix
#

Let’s construct an access control matrix mapping user roles to specific permission flags. We want to ensure that:

  1. Every role in the system is accounted for (Exhaustive Keys).
  2. The permission matrix is immutable at runtime (Readonly).
type UserRole = "admin" | "editor" | "viewer";

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

// 🟢 Compose: A Record of Roles mapped to Readonly Permissions
type AclMatrix = Record<UserRole, Readonly<Permissions>>;

const acl: AclMatrix = {
  admin: { canDelete: true, canEdit: true, canRead: true },
  editor: { canDelete: false, canEdit: true, canRead: true },
  viewer: { canDelete: false, canEdit: false, canRead: true },
};

// ❌ Compiler Error: Cannot assign to 'canEdit' because it is a read-only property.
// acl.viewer.canEdit = true;

4. Partial<Record<K, V>> (Optional Dictionaries)
#

If you are mapping a Union but you do not want to enforce that every key must be present (e.g., an overriding configuration where missing keys default to standard behavior), wrap the Record in Partial:

type UIComponent = "button" | "header" | "footer" | "sidebar";

// We don't have to provide a style override for every component.
type ThemeOverrides = Partial<Record<UIComponent, string>>;

const myTheme: ThemeOverrides = {
  button: "bg-blue-500 text-white", // Only overriding the button style
  // header, footer, and sidebar are safely omitted.
};

5. How Record<K, V> is Defined Under the Hood
#

To understand how Record enforces these rules, look at its core TypeScript definition:

type CustomRecord<K extends keyof any, T> = {
  [P in K]: T;
};
  1. K extends keyof any: The Key type K is constrained to valid object keys (string | number | symbol).
  2. [P in K]: T: It loops over every property P in the union K, and assigns it the exact value type T.

Summary & Next Steps
#

In this episode:

  • We modeled arbitrary dictionaries using Record<string, V>.
  • We enforced exhaustive key mapping by passing Unions as the Key parameter (Record<Union, V>).
  • We composed Record with Readonly<T> for access matrices and Partial<T> for optional configurations.
  • We analyzed the internal mapped type definition powering Record.

In Episode 36: Utility Types (Extract and Exclude), we will transition from transforming Objects to transforming Union types!

typescript - This article is part of a series.
Part 35: This Article