Level up your TypeScript with Record types
Tags: typescript, webdev
Published: 2023-06-28 15:07:55

**Written by [Matteo Di Pirro](https://blog.logrocket.com/author/matteodipirro/)✏️**

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?](#what-is-difference-between-record-tuple)
*   [Implementing the `Record` type](#implementing-record-type)
    *   [Identifying missing properties](#identifying-missing-properties)
    *   [Identifying undefined properties](#identifying-undefined-properties)
    *   [Accessing `Record` data](#accessing-record-data)
*   [Use case 1: Enforcing exhaustive case handling](#use-case-enforcing-exhaustive-case-handling)
*   [Use case 2: Enforcing type checking in applications that use generics](#use-case-enforcing-type-checking-application-generics)

## What’s the difference between a record and a tuple? <a name="what-is-difference-between-record-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](https://blog.logrocket.com/use-cases-named-tuples-typescript/), 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](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type):

> “`Record<Keys, Type>` constructs an object type whose property keys are `Keys` and whose property values are `Type`. 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 <a name="implementing-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:

```typescript
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 <a name="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 type `Record<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 type `Record<Course, CourseInfo>`”

### Accessing `Record` data <a name="accessing-record-data">

We can access data related to each `Course` as we would with any other dictionary:

```typescript
console.log(courses["Literature"])
```

The statement above prints the following output:

```json
{ "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 <a name="use-case-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](https://en.wikipedia.org/wiki/Factory_method_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:

```typescript
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:

```typescript
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 <a name="use-case-enforcing-type-checking-application-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](https://blog.logrocket.com/using-typescript-generics-create-reusable-components/). 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:

```typescript
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 the `Properties` 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:

```typescript
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:

```typescript
// 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:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
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

[Get started now](https://lp.logrocket.com/blg/signup)