Written by Matteo Di Pirro✏️
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.
Jump ahead:
- What’s the difference between a record and a tuple?
- Implementing the
Record
type - Use case 1: Enforcing exhaustive case handling
- Use case 2: Enforcing type checking in applications that use generics
What’s the difference between a record and a tuple?
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 since we can use their name to reference them.
That being said, at first, the Record
type in TypeScript might 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 take a look at an example to better understand how we can use the TypeScript Record
type.
Implementing the TypeScript Record
type
The 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 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
.
Identifying missing properties
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.
Identifying undefined properties
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. Since 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>
”
Accessing Record
data
We 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.
Use case 1: Enforcing exhaustive case handling
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 will not 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
.
Use case 2: Enforcing type checking in applications that use generics
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 actually makes sense. As we’ll see in a moment, the 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:
- The type of the key is
keyof Properties
, meaning that the keys in each object have to be the same as those defined by theProperties
generic type - The value of each of the keys will be the value of the corresponding property of the
Properties
record
Now 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)
Conclusion
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.
The Record
type is really powerful. Some of its use cases are rather niche, but it provides real value for our application’s code.
Get set up with LogRocket's modern Typescript error tracking in minutes:
1.Visit https://logrocket.com/signup/ to get an app ID.
2.Install LogRocket via NPM or script tag. LogRocket.init()
must be called client-side, not server-side.
NPM:
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Script Tag:
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
3.(Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
Top comments (0)