Record
typesEditor’s note: This article was last updated by Oyinkansola Awosan on 10 June 2024 to cover iterating over TypeScript record types using methods like forEach
, for…in
, and more, as well as to include advanced use cases for TypeScript record types.
In computer science, a record is a data structure holding a collection of fields, possibly with different types. In TypeScript, the Record
type simply allows us to define dictionaries, also referred to as key-value pairs, with a fixed type for the keys and a fixed type for the values.
In other words, the Record
type lets us define the type of a dictionary; that is, the names and types of its keys. In this article, we’ll explore the Record
type in TypeScript to better understand what it is and how it works. We’ll also investigate how to use it to handle enumeration, as well as how to use it with generics to understand the properties of the returned value when writing reusable code.
The Record
type in TypeScript may initially seem somewhat counterintuitive. In TypeScript, records have a fixed number of members (i.e., a fixed number of fields), and the members are usually identified by name. This is the primary way that records differ from tuples.
Tuples are groups of ordered elements, where the fields are identified by their position in the tuple definition. Fields in records, on the other hand, have names. Their position does not matter because we can use their name to reference them.
That being said, the Record
type in TypeScript might initially look unfamiliar. Here’s the official definition from the docs:
“
Record<Keys, Type>
constructs an object type whose property keys areKeys
and whose property values areType
. This utility can be used to map the properties of a type to another type.”
Let’s look at an example to better understand how we can use the TypeScript Record
type.
Record
typeThe power of TypeScript’s Record
type is that we can use it to model dictionaries with a fixed number of keys. For example, we could use the Record
type to create a model for university courses:
type Course = "Computer Science" | "Mathematics" | "Literature" interface CourseInfo { professor: string cfu: number } const courses: Record<Course, CourseInfo> = { "Computer Science": { professor: "Mary Jane", cfu: 12 }, "Mathematics": { professor: "John Doe", cfu: 12 }, "Literature": { professor: "Frank Purple", cfu: 12 } }
In this example, we defined a type named Course
that will list the names of classes and a type named CourseInfo
that will hold some general details about the courses. Then, we used a Record
type to match each Course
with its CourseInfo
.
So far, so good — it all looks like quite a simple dictionary. The real strength of the Record
type is that TypeScript will detect whether we missed a Course
.
Let’s say we didn’t include an entry for Literature
. We’d get the following error at compile time:
“Property
Literature
is missing in type{ "Computer Science": { professor: string; cfu: number; }; Mathematics: { professor: string; cfu: number; }; }
but required in typeRecord<Course, CourseInfo>
.”
In this example, TypeScript is clearly telling us that Literature
is missing.
TypeScript will also detect if we add entries for values that are not defined in Course
. Let’s say we added another entry in Course
for a History
class. Because we didn’t include History
as a Course
type, we’d get the following compilation error:
“Object literal may only specify known properties, and
"History"
does not exist in typeRecord<Course, CourseInfo>
.”
Record
dataWe can access data related to each Course
as we would with any other dictionary:
console.log(courses["Literature"])
The statement above prints the following output:
{ "teacher": "Frank Purple", "cfu": 12 }
Let’s proceed to take a look at some cases where the Record
type is particularly useful.
When writing modern applications, it’s often necessary to run different logic based on some discriminating value. A perfect example is the factory design pattern, where we create instances of different objects based on some input. In this scenario, handling all cases is paramount.
The simplest (and somehow naive) solution would probably be to use a switch
construct to handle all the cases:
type Discriminator = 1 | 2 | 3 function factory(d: Discriminator): string { switch(d) { case 1: return "1" case 2: return "2" case 3: return "3" default: return "0" } }
If we add a new case to Discriminator
, however, due to the default
branch, TypeScript won’t tell us we’ve failed to handle the new case in the factory
function. Without the default
branch, this would not happen; instead, TypeScript would detect that a new value had been added to Discriminator
.
We can leverage the power of the Record
type to fix this:
type Discriminator = 1 | 2 | 3 function factory(d: Discriminator): string { const factories: Record<Discriminator, () => string> = { 1: () => "1", 2: () => "2", 3: () => "3" } return factories[d]() } console.log(factory(1))
The new factory
function simply defines a Record
matching a Discriminator
with a tailored initialization function, inputting no arguments and returning a string
. Then, factory
just gets the right function, based on the d: Discriminator
, and returns a string
by calling the resulting function. If we now add more elements to Discriminator
, the Record
type will ensure that TypeScript detects missing cases in factories
.
Generics allow us to write code that is abstract over actual types. For example, Record<K, V>
is a generic type. When we use it, we have to pick two actual types: one for the keys (K
) and one for the values (V
).
Generics are extremely useful in modern programming, as they enable us to write highly reusable code. The code to make HTTP calls or query a database is normally generic over the type of the returned value. This is very nice, but it comes at a cost because it makes it difficult for us to know the actual properties of the returned value.
We can solve this by leveraging the Record
type:
class Result<Properties = Record<string, any>> { constructor( public readonly properties: Record< keyof Properties, Properties[keyof Properties] > ) {} }
Result
is a bit complex. In this example, we declare it as a generic type where the type parameter, Properties
, defaults to Record<string, any>
.
Using any
here might look ugly, but it makes sense. As we’ll see in a moment, Record
will map property names to property values, so we can’t really know the type of the properties in advance. Furthermore, to make it as reusable as possible, we’ll have to use the most abstract type TypeScript has — any
, indeed!
The constructor
leverages some TypeScript syntactic sugar to define a read-only property, which we’ve aptly named properties
. Notice the definition of the Record
type:
keyof Properties
, meaning that the keys in each object have to be the same as those defined by the Properties
generic typeProperties
recordNow that we’ve defined our main wrapper type, we can experiment with it. The following example is very simple, but it demonstrates how we can use Result
to have TypeScript check the properties of a generic type:
interface CourseInfo { title: string professor: string cfu: number } const course = new Result<Record<string, any>>({ title: "Literature", professor: "Mary Jane", cfu: 12 }) console.log(course.properties.title) //console.log(course.properties.students) <- this does not compile!
In the above code, we define a CourseInfo
interface that looks similar to what we saw earlier. It simply models the basic information we’d like to store and query: the name of the class, the name of the professor
, and the number of credits.
Next, we simulate the creation of a course
. This is just a literal value, but you can imagine it to be the result of a database query or an HTTP call.
Notice that we can access the course
properties in a type-safe manner. When we reference an existing property, such as title
, it compiles and works as expected. When we attempt to access a nonexistent property, such as students
, TypeScript detects that the property is missing in the CourseInfo
declaration and the call does not compile.
This is a powerful feature we can leverage in our code to ensure the values we fetch from external sources comply with our expected set of properties. Note that if course
had more properties than those defined by CourseInfo
, we could still access them. In other words, the following snippet would work:
// CourseInfo and Result as above const course = new Result<Record<string, any>>({ title: "Literature", professor: "Mary Jane", cfu: 12, webpage: "https://..." }) console.log(course.properties.webpage)
The map
type is a TypeScript feature that enables you to create new types by transforming existing types.
In the following table, we’ll compare Typescript Record
and Map
types based on several features:
Record types | Map types | |
---|---|---|
Performance | Great for static keys and typically faster when trying to access values. | Optimized for dynamic data but may be a bit slower for key lookups. |
Type safety | Strong type safety for both key and values. At compile time, TypeScript ensures that the keys and values conform to the specified types. | Maps is a bit flexible, allowing keys of any type, but some type safety is offered for values.
Weaker type safety compared to Record. |
Use cases | It is great for scenarios where type safety is top priority and the keys are already known. | Perfect for scenarios where flexibility is required in the key types.
Also great for scenarios where order of insertion is important as it is preserved with Maps. |
Syntax | Record<Key, Value> |
new Map<Key, Value>() |
Key types | Key types are fixed and limited. Typically strings and numbers. | Allows keys of any type, ranging from strings, numbers, objects, functions, etc. |
In this section, we will explore various methods to iterate over record types, including forEach
, for...in
, Object.keys()
, and Object.values()
. Understanding how to iterate over TypeScript record types is crucial for effectively accessing the data within these structures.
forEach
To use forEach
with a record type, you first need to convert the record to an array of key-value pairs. This can be done using Object.entries()
:
type Course = "Computer Science" | "Mathematics" | "Literature"; interface CourseInfo { professor: string; cfu: number; } const courses: Record<Course, CourseInfo> = { "Computer Science": { professor: "Mary Jane", cfu: 12 }, "Mathematics": { professor: "John Doe", cfu: 12 }, "Literature": { professor: "Frank Purple", cfu: 12 }, }; Object.entries(courses).forEach(([key, value]) => { console.log(`${key}: ${value.professor}, ${value.cfu}`); });
for...in
The for...in
loop allows iterating over the keys of a record:
for (const key in courses) { if (courses.hasOwnProperty(key)) { const course = courses[key as Course]; console.log(`${key}: ${course.professor}, ${course.cfu}`); } }
Object.keys()
Object.keys()
returns an array of the record’s keys, which can then be iterated over using forEach
or any loop:
Object.keys(courses).forEach((key) => { const course = courses[key as Course]; console.log(`${key}: ${course.professor}, ${course.cfu}`); });
Object.values()
Object.values()
returns an array of the record’s values, which can be iterated over:
Object.values(courses).forEach((course) => { console.log(`${course.professor}, ${course.cfu}`); });
Object.entries()
Object.entries()
returns an array of key-value pairs, allowing you to use array destructuring within the loop:
Object.entries(courses).forEach(([key, value]) => { console.log(`${key}: ${value.professor}, ${value.cfu}`); });
Typescript’s Record
type can be utilized for more advanced patterns such as selective type mapping with the Pick
type and implementing dynamic key-value pairs. These use cases provide additional flexibility and control when working with complex data structures.
Pick
type with Record
for selective type mappingThe Pick
type in TypeScript allows us to create a new type by selecting specific properties from an existing type. When combined with the Record
type, it becomes a powerful tool for creating dictionaries with only a subset of properties.
Suppose we have a CourseInfo
interface with several properties but we only want to map a subset of these properties in our Record
:
interface CourseInfo { professor: string; cfu: number; semester: string; students: number; } type SelectedCourseInfo = Pick<CourseInfo, "professor" | "cfu">; type Course = "Computer Science" | "Mathematics" | "Literature"; const courses: Record<Course, SelectedCourseInfo> = { "Computer Science": { professor: "Mary Jane", cfu: 12 }, "Mathematics": { professor: "John Doe", cfu: 12 }, "Literature": { professor: "Frank Purple", cfu: 12 }, };
In the above example, we used Pick<CourseInfo, "professor" | "cfu">
to create a new type SelectedCourseInfo
that only includes the professor
and the cfu
properties from CourseInfo
. Then, we defined a Record
type that maps each Course
to SelectedCourseInfo
.
Record
for dynamic key-value pairsTypeScript’s Record
type can also be used to implement dynamic key-value pairs, which is useful for creating dictionaries where the keys are not known in advance.
Consider a scenario where we have dynamic user preferences with keys and values that can change over time:
type PreferenceKey = string; // Dynamic keys type PreferenceValue = string | boolean | number; // Values interface UserPreferences { [key: string]: PreferenceValue; } const userPreferences: Record<PreferenceKey, PreferenceValue> = { theme: "dark", notifications: true, fontSize: 14, }; // Updating a preference userPreferences.language = "English"; // Iterating over preferences Object.entries(userPreferences).forEach(([key, value]) => { console.log(`${key}: ${value}`); });
In the above, we defined PreferenceKey
as a string and PreferenceValue
as a union type that can be a string, Boolean, or number. The UserPreferences
interface uses an index signature to represent dynamic keys with PeferenceValue
as their type. We then use Record<PreferenceKey
, and PreferenceValue>
to create a userPreferences
dictionary that can store and iterate over dynamic user preferences.
Record
with other utility typesTypeScript’s utility types can be combined with Record
to create more complex and type-safe data structures.
ReadOnly
with Record
The ReadOnly
type makes all the properties of a type read-only. This is especially useful when you want to ensure that dictionary entries cannot be modified:
type ReadonlyCourseInfo = Readonly<CourseInfo>; const readonlyCourses: Record<Course, ReadonlyCourseInfo> = { "Computer Science": { professor: "Mary Jane", cfu: 12, semester: "Fall", students: 100 }, "Mathematics": { professor: "John Doe", cfu: 12, semester: "Spring", students: 80 }, "Literature": { professor: "Frank Purple", cfu: 12, semester: "Fall", students: 60 }, }; // Trying to modify a readonly property will result in a compile-time error // readonlyCourses["Computer Science"].cfu = 14; // Error: Cannot assign to 'cfu' because it is a read-only property.
In the code above, Readonly<CourseInfo>
ensures that all properties of CourseInfo
are read-only, preventing modifications. Trying to modify a read-only property will result in a compile-time error. The below should fail/throw an error:
readonlyCourses["Computer Science"].cfu = 14; // Error: Cannot assign to 'cfu' because it is a read-only property.
Partial
with Record
The Partial
type makes all properties of a type optional. This is especially useful when you want to create a dictionary where some entries may not have all properties defined:
type PartialCourseInfo = Partial<CourseInfo>; const partialCourses: Record<Course, PartialCourseInfo> = { "Computer Science": { professor: "Mary Jane" }, "Mathematics": { cfu: 12 }, "Literature": {}, };
In the code above, Partial<CourseInfo>
makes all properties of CourseInfo
optional, allowing us to create a Record
where some courses may not have all properties defined or even have none of the properties defined.
In this article, we explored one of TypeScript’s inbuilt types, Record<K, V>
. We looked at basic usage for the Record
type and investigated how it behaves. Then, we examined two prominent use cases for the Record
type.
In the first use case, we investigated how we can use the Record
type to ensure that we handle the cases of an enumeration. In the second use case, we explored how we can enforce type checking on the properties of an arbitrary object in an application with generic types.
In addition, we covered how to iterate over TypeScript record types using various methods such as forEach
, for...in
, Object.keys()
, and Object.values()
. These methods allow you to effectively manipulate and access the data within record types.
By leveraging TypeScript’s utility types, such as Pick
, Partial
, and Readonly
, in combination with the Record
type, we can create more complex and type-safe data structures. Understanding these advanced patterns enhances your ability to work with TypeScript.
The Record
type is really powerful. Though some of its use cases are rather niche, it provides great value to our application’s code.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
One Reply to "Level up your TypeScript with <code>Record</code> types"
The final examples never use CourseInfo interface?