Understanding TypeScript's Record Type

Table of Content
- Syntax:
- Practical Examples of Record in Use
- Basic Usage
- Restricting Keys with Literal Types
- Nested Record Types
- Using Record to Create Enum-like Structures
- Combining Record with Other Utility Types
- Using Partial<Record<...>>
- Combining Record with Readonly
- Real-world Example: Permissions System
- Why Use Record?
- Summary
- Key Takeaways
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
WhileRecord
is very useful, it’s not always the best choice. For more complex, nested objects or when you need individual control over properties, consider usinginterface
ortype
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 withPartial
,Readonly
, and other utility types to add flexibility.
Using Record
effectively can make your codebase cleaner, safer, and more maintainable.