Roy Lopez
PersistDev.blog
#typescript

Understanding TypeScript's Record Type

Understanding TypeScript's Record Type
0 views
5 min read
#typescript

TypeScript’s Record utility type is a powerful tool to create mapped types concisely. With Record, you can specify the exact shape of an object by defining both the types of its keys and values, which is helpful when creating objects with specific keys and controlling the type of associated values.

What is the Record Type?
In essence, Record<K, T> is a TypeScript utility type where:

  • K is a union of literal types representing the keys of the object.
  • T is the type for each value associated with those keys.

This type essentially creates an object type where all keys are of type K, and all values are of type T.

Syntax:

Record<K extends keyof any, T>

Practical Examples of Record in Use

Basic Usage

Let's start with a simple example where we create a Record type for an object that maps strings to numbers:

const scores: Record<string, number> = {
  Alice: 10,
  Bob: 15,
  Charlie: 12,
};

Here, scores is an object where each key (a string) maps to a number.

Restricting Keys with Literal Types

One of the strengths of Record is its ability to restrict keys to specific literal types. For instance, if you’re building an app with a finite set of supported languages, you can use Record to type-check that only those languages are used as keys:

type Language = "en" | "es" | "fr";

const translations: Record<Language, string> = {
  en: "Hello",
  es: "Hola",
  fr: "Bonjour",
};

In this case, TypeScript enforces that the keys of translations are exactly "en", "es", or "fr"—no more, no less.

Nested Record Types

The Record type is flexible enough to handle nested objects. Suppose you’re creating a configuration object for different environments, each with specific settings:

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

type Config = {
  apiUrl: string;
  debug: boolean;
};

const envConfig: Record<Environment, Config> = {
  development: { apiUrl: "http://localhost:3000", debug: true },
  staging: { apiUrl: "https://staging.api.com", debug: false },
  production: { apiUrl: "https://api.com", debug: false },
};

This setup allows TypeScript to ensure that each environment has a corresponding Config object, with the apiUrl as a string and debug as a boolean.

Using Record to Create Enum-like Structures

You can also use Record to create more structured, enum-like objects. For example, suppose you need a set of statuses for an order, each with a different description:

type OrderStatus = "pending" | "shipped" | "delivered" | "returned";

const orderStatusMessages: Record<OrderStatus, string> = {
  pending: "Your order is pending.",
  shipped: "Your order has been shipped.",
  delivered: "Your order has been delivered.",
  returned: "Your order has been returned.",
};

Combining Record with Other Utility Types

Using Partial<Record<...>>

To allow a Record type with optional keys, you can use Partial<Record<...>>, making all properties optional:

type FeatureFlags = "darkMode" | "betaUser" | "offlineSupport";

const features: Partial<Record<FeatureFlags, boolean>> = {
  darkMode: true,
  betaUser: false,
  // `offlineSupport` is optional here
};

Combining Record with Readonly

To make a Record immutable, use Readonly<Record<...>>:

type Currency = "USD" | "EUR" | "JPY";

const currencySymbols: Readonly<Record<Currency, string>> = {
  USD: "$",
  EUR: "€",
  JPY: "¥",
};

// Trying to modify this will throw an error
// currencySymbols.USD = "US$"; // Error

Real-world Example: Permissions System

A more complex example could be a permissions system for an application where you define roles and their allowed actions:

type Role = "admin" | "user" | "guest";
type Action = "read" | "write" | "delete";

const permissions: Record<Role, Action[]> = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

Here, each role (key) has an array of allowed actions as its value, providing a type-safe way to define and manage permissions.

Why Use Record?

Using Record has several advantages:

  • Type Safety: Ensures objects conform to specific key-value requirements.
  • Code Clarity: Documents and enforces the intended data structure.
  • Enhanced Readability: Reduces the need for custom types or interfaces for simple maps, making the codebase cleaner.

When Not to Use Record
While Record is very useful, it’s not always the best choice. For more complex, nested objects or when you need individual control over properties, consider using interface or type with more granular control.

Summary

The Record type is incredibly versatile in TypeScript, enabling you to create structured, type-safe mappings in a clean and concise way. From language translations to role-based access control, Record can simplify and streamline your TypeScript code.

Key Takeaways

  • Define Mapped Types: Record allows you to specify both the keys and values of an object.
  • Restrict Keys: Use literal types to ensure only specific values are allowed.
  • Combine with Utility Types: Record can work with Partial, Readonly, and other utility types to add flexibility.

Using Record effectively can make your codebase cleaner, safer, and more maintainable.

Loading...