TypeScript’s Record
type simplifies managing object structures with consistent value types. This guide covers the essentials of Record
, including its definition, syntax, and how it differs from other types like tuples. We’ll learn how to define and use Record
in practical scenarios such as enforcing exhaustive case handling and mapping enums. Additionally, we’ll explore advanced uses by combining Record
with utility types like Partial
, Pick
, and Readonly
.
Introduction
The Record
type is a utility type that allows us to create an object type with specified keys and a uniform value type. This type is particularly useful for defining mappings and ensuring that all values in an object conform to a single type.
Definition of Record Type
The official definition from the TypeScript documentation is:
Record<Keys, Type>
Here:
Keys
represent the set of keys in the record, which can be a union of string literals or a type derived from a union.Type
is the type of the values associated with those keys.
For example, Record<string, number>
defines an object where every key is a string and every value is a number. This type ensures that all properties of the object have the same value type, but the keys can be varied.
Comparison Between a Record and a Tuple
Both Record
and tuples are used to handle collections of data, but they serve different purposes. Even as they store multiple values, they differ in structure and usage. A Record has named properties with a fixed type, whereas a tuple is an ordered list of elements identified by their position. Here’s a simple comparison:
- Record. Creates an object type where all values have the same type, but the keys can be flexible. This is useful for mapping keys to values and ensuring that all keys adhere to a specific type.
- Tuple. Defines an array with a fixed number of elements, where each element can have a different type. Tuples are used when we need a fixed-size collection with specific types for each position.
For example, consider the following.
Here’s a Record
type, which maps string keys to number values:
type AgeMap = Record<string, number>;
A tuple type represents an array with a string (name) and a number (age) in a fixed position:
type Person = [string, number];
Basic Usage of Record Type
The Record
type provides a simple and efficient way to map keys to values. It’s particularly useful when we need to define objects with specific key–value pairs where the keys are of a particular type, and the values are of another type.
Here are some basic ways to use the Record
type to define and create structured data.
Defining a Record
To define a Record
, we specify the types for the keys and values. The example below defines an object where each key is a string, and each value is also a string. This could be used for a generic map of user data:
type User = Record<string, string>;
Creating a Record
Type Example
Some websites have various subdomains. Let’s assume each of these subdomains requires some level of admin access and create a Record
type for storing different admin roles and their corresponding access levels. Here, UserRoles
and UserStatus
are Record
types where the keys are specific string literals (admin
, blogAdmin
, docsAdmin
, active
, inactive
, suspended
), and the values are strings that describe each role and status.
First, we define a Record
type UserRoles
with specific admin roles as keys and their descriptions as values. The UserRoles
type ensures that any object of this type will have keys admin
, blogAdmin
, and docsAdmin
, with string values describing each role. The roles
object adheres to this type by providing descriptions for each admin role:
type UserRoles = Record<'admin' | 'blogAdmin' | 'docsAdmin', string>;
const roles: UserRoles = {
admin: 'General Administrator with access to all areas.',
blogAdmin: 'Administrator with access to blog content.',
docsAdmin: 'Administrator with access to documentation.'
};
Next, we define a Record
type UserStatus
with specific statuses as keys and their descriptions as values. The UserStatus
type ensures that any object of this type will have keys active
, inactive
, and suspended
, with string values describing each status. The userStatus
object adheres to this type by providing descriptions for each status:
type UserStatus = Record<'active' | 'inactive' | 'suspended', string>;
const userStatus: UserStatus = {
active: 'User is currently active and can use all features.',
inactive: 'User is currently inactive and cannot access their account.',
suspended: 'User account is suspended due to policy violations.'
};
By creating Record
types in this way, we ensure that the admin roles and user statuses are well defined and consistent throughout the application.
Practical Use Cases of Record Type
In this section, we’ll review several practical use cases of the Record
type to demonstrate its versatility and effectiveness in different scenarios.
Use Case 1: Enforcing Exhaustive Case Handling
Using Record
to define a mapping between case values and messages allows us to handle each possible case explicitly. This ensures that all cases are covered and that any missing cases will result in compile-time errors.
In the example below, statusMessages
is a Record
where the keys are specific Status
values ('pending'
, 'completed'
, 'failed'
), and each key maps to a corresponding message. The getStatusMessage
function uses this record to return the appropriate message based on the status
parameter. This approach guarantees that all statuses are handled correctly and consistently.
Example:
type Status = 'pending' | 'completed' | 'failed';
interface StatusInfo {
message: string;
severity: 'low' | 'medium' | 'high';
retryable: boolean;
}
const statusMessages: Record<Status, StatusInfo> = {
pending: {
message: 'Your request is pending.',
severity: 'medium',
retryable: true,
},
completed: {
message: 'Your request has been completed.',
severity: 'low',
retryable: false,
},
failed: {
message: 'Your request has failed.',
severity: 'high',
retryable: true,
},
};
function getStatusMessage(status: Status): string {
const info = statusMessages[status];
return `${info.message} Severity: ${info.severity}, Retryable: ${info.retryable}`;
}
//Cases where the request was successful.
console.log(getStatusMessage('completed')); // Your request has been completed. Severity: low, Retryable: false
Use Case 2: Enforcing Type Checking in Applications Using Generics
Generics in TypeScript allow for flexible and reusable code. When combined with Record
, generics can help enforce type checking and ensure that objects conform to specific structures.
By using generics with Record
, we can create functions or utilities that generate objects with a specific set of keys and a consistent value type. This approach enhances type safety and reusability in our codebase.
In the example below, the createRecord
function takes an array of keys and a value, and it returns a Record
where each key maps to the provided value. This function uses generics (K
for keys and T
for value type) to ensure that the resulting Record
has the correct structure.
Example:
function createRecord<K extends string, T>(keys: K[], value: T): Record<K, T> {
const record: Partial<Record<K, T>> = {};
keys.forEach(key => record[key] = value);
return record as Record<K, T>;
}
interface RoleInfo {
description: string;
permissions: string[];
}
const userRoles = createRecord(['admin', 'editor', 'viewer'], {
description: 'Default role',
permissions: ['read'],
});
console.log(userRoles);
/*
//Output:
{
admin: { description: 'Default role', permissions: ['read'] },
editor: { description: 'Default role', permissions: ['read'] },
viewer: { description: 'Default role', permissions: ['read'] }
}
*/
Use Case 3: Mapping Enums to Data
Using Record
to map enums to data allows us to create a lookup table where each enum value is associated with specific information. This is particularly useful for scenarios like configuring settings based on enum values.
In this example, colorHex
is a Record
that maps each Color
enum value to its corresponding hexadecimal color code. This approach provides a clear and type-safe way to handle color-related data based on enum values.
Example:
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE',
Yellow = 'YELLOW'
}
interface ColorInfo {
hex: string;
rgb: string;
complementary: string;
}
const colorHex: Record<Color, ColorInfo> = {
[Color.Red]: {
hex: '#FF0000',
rgb: 'rgb(255, 0, 0)',
complementary: '#00FFFF',
},
[Color.Green]: {
hex: '#00FF00',
rgb: 'rgb(0, 255, 0)',
complementary: '#FF00FF',
},
[Color.Blue]: {
hex: '#0000FF',
rgb: 'rgb(0, 0, 255)',
complementary: '#FFFF00',
},
[Color.Yellow]: {
hex: '#FFFF00',
rgb: 'rgb(255, 255, 0)',
complementary: '#0000FF',
},
};
console.log(colorHex[Color.Green]); //Output: { hex: '#00FF00', rgb: 'rgb(0, 255, 0)', complementary: '#FF00FF' }
Use Case 4: Creating Lookup Tables
A lookup table using Record
helps in mapping keys (such as identifiers, names) to specific values (such as descriptions, codes). This can be useful for various applications, including configurations, translations, and many other things.
Here, countryCode
is a Record
that maps country codes to their respective country names, population, capitals and continents. This lookup table allows for quick and type-safe retrieval of country names and populations based on country codes.
Example:
type CountryCode = "US" | "CA" | "MX" | "JP";
interface CountryInfo {
name: string;
population: number;
capital: string;
continent: string;
}
const countryLookup: Record<CountryCode, CountryInfo> = {
US: {
name: "United States",
population: 331000000,
capital: "Washington D.C.",
continent: "North America",
},
CA: {
name: "Canada",
population: 37700000,
capital: "Ottawa",
continent: "North America",
},
MX: {
name: "Mexico",
population: 128000000,
capital: "Mexico City",
continent: "North America",
},
JP: {
name: "Japan",
population: 126300000,
capital: "Tokyo",
continent: "Asia",
},
};
console.log(countryLookup.US);
/*
//Output:
{
name: "United States",
population: 331000000,
capital: "Washington D.C.",
continent: "North America"
}
*/
console.log(countryLookup.US.population);//Output: 331000000,
Iterating Over Record
Types
Iterating over Record
types is important for accessing and manipulating the data within data structures. Let’s create a sample data and show various methods on how we can iterate over the TypeScript Record
types.
Sample Data:
interface Course {
professor: string;
credits: number;
students: string[];
}
interface Courses {
[key: string]: Course;
}
const courses: Courses = {
Math101: {
professor: "Dr. Eze",
credits: 3,
students: ["Emmanuel", "Bob", "Charlie"],
},
History201: {
professor: "Dr. Jones",
credits: 4,
students: ["Dave", "Eve"],
},
};
Using forEach
. To use forEach
with a Record
, convert it to an array of key-value pairs:
Object.entries(courses).forEach(([key, value]) => {
console.log(`${key}: ${value.professor}, ${value.credits}`);
value.students.forEach(student => {
console.log(`Student: ${student}`);
});
});
/*
//Output:
Math101: Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
History201: Dr. Jones, 4
Student: Dave
Student: Eve
*/
Using for...in
. The for...in
loop iterates over the keys of a Record
:
for (const key in courses) {
if (courses.hasOwnProperty(key)) {
const course = courses[key];
console.log(`${key}: ${course.professor}, ${course.credits}`);
course.students.forEach(student => {
console.log(`Student: ${student}`);
});
}
}
/*
//Output:
Math101: Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
History201: Dr. Jones, 4
Student: Dave
Student: Eve
*/
Using Object.keys()
. Object.keys()
returns an array of the Record
’s keys:
Object.keys(courses).forEach((key) => {
const course = courses[key];
console.log(`${key}: ${course.professor}, ${course.credits}`);
course.students.forEach(student => {
console.log(`Student: ${student}`);
});
});
/*
//Output:
Math101: Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
History201: Dr. Jones, 4
Student: Dave
Student: Eve
*/
Using Object.values()
. Object.values()
returns an array of the Record
’s values:
Object.values(courses).forEach((course) => {
console.log(`${course.professor}, ${course.credits}`);
course.students.forEach(student => {
console.log(`Student: ${student}`);
});
});
/*
//Output:
Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
Dr. Jones, 4
Student: Dave
Student: Eve
*/
Advanced Usage and Utility Types with Record
The Record
type can be combined with other utility types to achieve greater flexibility and type safety. This section exposes advanced usage patterns, demonstrating how Record
can work with utility types like Pick
, Readonly
, and Partial
.
Combining Record
with Pick
for Selective Type Mapping
The Pick
utility type allows us to create a new type by selecting specific properties from an existing type. This is useful when we want to work with only a subset of properties from a larger type.
Here, we created a new type SelectedProductInfo
by picking only the name
and price
properties from the ProductInfo
interface, and then using Record
to map different products to this new type:
interface ProductInfo {
name: string;
price: number;
category: string;
}
type SelectedProductInfo = Pick<ProductInfo, "name" | "price">;
type Product = 'Laptop' | 'Smartphone' | 'Tablet';
const products: Record<Product, SelectedProductInfo> = {
"Laptop": { name: "Dell XPS 15", price: 1500 },
"Smartphone": { name: "iPhone 12", price: 999 },
"Tablet": { name: "iPad Pro", price: 799 }
};
Combining Record
with Readonly
for Immutable Properties
The Readonly
utility type ensures that properties can’t be modified after they’re set. This is useful for creating immutable data structures.
The ReadonlyProductInfo
type in the example below makes all properties of ProductInfo
immutable, ensuring that the details of each product can’t be changed once they’re defined:
type ReadonlyProductInfo = Readonly<ProductInfo>;
const readonlyProducts: Record<Product, ReadonlyProductInfo> = {
"Laptop": { name: "Dell XPS 15", price: 1500, category: "Electronics" },
"Smartphone": { name: "iPhone 12", price: 999, category: "Electronics" },
"Tablet": { name: "iPad Pro", price: 799, category: "Electronics" }
};
Combining Record
with Partial
for Optional Properties
The Partial
utility type makes all properties of a type optional. This is useful for scenarios where not all properties might be known or required at the same time.
Here, the PartialProductInfo
type allows us to create products with some or none of the properties defined in ProductInfo
, providing flexibility in how product information is specified:
type PartialProductInfo = Partial<ProductInfo>;
const partialProducts: Record<Product, PartialProductInfo> = {
"Laptop": { name: "Dell XPS 15" },
"Smartphone": { price: 999 },
"Tablet": {}
};
Combining Record
with Record
for Nested Mapping
Another advanced usage involves combining Record
types to create nested mappings, which can be particularly useful for managing complex data structures.
In this example, storeInventory
uses nested Record
types to map departments to their respective products and details, demonstrating how Record
can be combined for more complex data management:
type Department = 'Electronics' | 'Furniture';
type ProductDetails = Record<Product, ProductInfo>;
const storeInventory: Record<Department, ProductDetails> = {
"Electronics": {
"Laptop": { name: "Dell XPS 15", price: 1500, category: "Electronics" },
"Smartphone": { name: "iPhone 12", price: 999, category: "Electronics" },
"Tablet": { name: "iPad Pro", price: 799, category: "Electronics" }
},
"Furniture": {
"Chair": { name: "Office Chair", price: 200, category: "Furniture" },
"Table": { name: "Dining Table", price: 500, category: "Furniture" },
"Sofa": { name: "Living Room Sofa", price: 800, category: "Furniture" }
}
};
Conclusion
The Record
type is a versatile tool for managing and structuring object types as it allows us to define clear mappings between keys and values, ensuring type safety and consistency in our code.
For more detailed information, refer to the TypeScript documentation and review other additional resources like Total TypeScript and TypeScript Tutorial to deepen your understanding of TypeScript’s Record
type system.
Emmanuel is a passionate software developer who finds joy in both writing and problem-solving.