---
title: "How to Use the Record Type in TypeScript"
date: "2023-08-11"
slug: "how-to-use-record-type-in-typescript"
author: "Dany Paredes"
canonical: "https://danywalls.com/how-to-use-record-type-in-typescript"
description: "Learn how to use TypeScript's Record<K, T> utility type. Compare Record with index signatures and Maps, and explore practical patterns with union types."
---


When building applications in JavaScript, we frequently store data in key-value dictionaries: configuration maps, status lookups, and cache tables.

In plain JavaScript, any key can be added, misspelled, or read without warning:

```javascript
const userRoles = {
  admin: 'Full Access',
  editor: 'Edit Content',
};

// Misspelled key returns undefined silently!
console.log(userRoles.admn);
```

How do we make key-value objects strictly typed, predictable, and self-documenting in TypeScript?

The answer is TypeScript's built-in **`Record<Keys, Type>`** utility type. In this post, I will show you how `Record` works, how to use it with union types, and when to choose `Record` over index signatures and `Map`.

---

## 1. What Is the `Record<Keys, Type>` Utility Type?

The `Record<Keys, Type>` utility type constructs an object type where:
- The keys must match type `Keys` (can be `string`, `number`, `symbol`, or a union of literals).
- The values must match type `Type`.

Under the hood, TypeScript defines `Record` with a mapped type:

```typescript
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
```

You are telling TypeScript: *"I want an object where every property key belongs to `K`, and every property value is of type `T`."*

Let us see how this works with a simple example.

---

## 2. Basic Example: Mapping Keys to Values

Imagine we want to track championship trophies for sports teams:

```typescript
type TeamName = 'Lakers' | 'Warriors' | 'Bulls' | 'Celtics';

// Construct the Record type
type ChampionshipRecord = Record<TeamName, number>;
```

Now we define our object using `ChampionshipRecord`:

```typescript
const nbaChampionships: ChampionshipRecord = {
  Lakers: 17,
  Warriors: 7,
  Bulls: 6,
  Celtics: 18,
};

console.log(`Lakers trophies: ${nbaChampionships.Lakers}`);
```

If you misspell a team name or forget to include one of the teams, TypeScript flags the error during compilation:

```typescript
// Error: Property 'Celtics' is missing in type...
const incompleteRecords: ChampionshipRecord = {
  Lakers: 17,
  Warriors: 7,
  Bulls: 6,
};
```

This guarantee is what makes `Record` so useful: it forces your dictionary to be **exhaustive**.

---

## 3. Practical Pattern: Handling Application State and Status Handlers

A common real-world scenario is mapping order status codes to human-readable UI badges or handlers:

```typescript
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

interface BadgeConfig {
  label: string;
  color: 'yellow' | 'blue' | 'purple' | 'green' | 'red';
}

const statusBadgeMap: Record<OrderStatus, BadgeConfig> = {
  pending: { label: 'Payment Pending', color: 'yellow' },
  processing: { label: 'In Preparation', color: 'blue' },
  shipped: { label: 'Out for Delivery', color: 'purple' },
  delivered: { label: 'Completed', color: 'green' },
  cancelled: { label: 'Order Cancelled', color: 'red' },
};
```

If a product manager later introduces a new status to `OrderStatus` (like `'refunded'`), TypeScript will immediately highlight `statusBadgeMap` with a compile-time error. You will never forget to update a badge in your UI.

What if you do not want to provide every single key upfront? That leads us to combining `Record` with other utility types.

---

## 4. Combining `Record` with `Partial` for Optional Dictionaries

Sometimes you have a large set of possible keys, but your object only holds a subset at any given time (such as an in-memory user cache):

```typescript
type UserId = string;

interface UserProfile {
  id: string;
  name: string;
  email: string;
}

// Partial allows keys to be optional
type UserCache = Partial<Record<UserId, UserProfile>>;

const activeUsers: UserCache = {
  'usr_101': { id: 'usr_101', name: 'Dany', email: 'dany@example.com' },
};
```

Using `Partial<Record<K, T>>` gives you the flexibility of an optional dictionary while maintaining full type checking on all inserted values.

---

## 5. Advanced Pattern: Template Literal Keys in Records

In modern TypeScript, you can combine `Record` with template literal types to enforce key naming conventions:

```typescript
type Environment = 'dev' | 'staging' | 'prod';
type ConfigKey = `API_URL_${Uppercase<Environment>}`;

// ConfigKey is "API_URL_DEV" | "API_URL_STAGING" | "API_URL_PROD"
const apiEndpoints: Record<ConfigKey, string> = {
  API_URL_DEV: 'https://dev.api.danywalls.com',
  API_URL_STAGING: 'https://staging.api.danywalls.com',
  API_URL_PROD: 'https://api.danywalls.com',
};
```

This pattern ensures team members follow exact naming conventions when declaring environment settings or event handlers.

---

## 6. Comparison: `Record<K, T>` vs Index Signatures vs `Map<K, V>`

Developers often ask when to use a `Record`, an index signature (`{ [key: string]: T }`), or JavaScript's native `Map`.

Here is a side-by-side comparison:

| Feature | `Record<K, T>` | Index Signature (`{ [k: string]: T }`) | JavaScript `Map<K, V>` |
| :--- | :--- | :--- | :--- |
| **Supported Key Types** | `string`, `number`, `symbol`, or unions | `string`, `number`, `symbol` (no unions) | Any type (objects, functions, primitives) |
| **Exhaustive Key Check** | Yes (when using a union of keys) | No (allows any arbitrary key) | No |
| **JSON Serialization** | Native (`JSON.stringify`) | Native (`JSON.stringify`) | Requires custom conversion |
| **Iteration Order** | Object property order | Object property order | Guaranteed insertion order |
| **Performance with Frequent Deletions** | Moderate | Moderate | High (optimized for mutations) |

### When to Pick Each:
- **Use `Record<K, T>`**: When you have known literal keys, static configs, or want compile-time exhaustiveness.
- **Use Index Signature**: When the keys are completely open-ended strings (for example, parsing dynamic query parameters).
- **Use `Map<K, V>`**: When you have non-string keys, need frequent additions and removals, or need fast `.size` lookups.

---

## 7. Gotcha: Safe Lookups with `noUncheckedIndexedAccess`

By default, TypeScript assumes that accessing any property on a `Record<string, T>` will always return `T`:

```typescript
const scores: Record<string, number> = {
  alice: 95,
};

// TypeScript thinks score is number, but at runtime it is undefined!
const score = scores['bob'];
console.log(score.toFixed(2)); // Runtime TypeError: Cannot read properties of undefined!
```

To prevent this runtime crash, enable `noUncheckedIndexedAccess` in your `tsconfig.json`:

```json
{
  "compilerOptions": {
    "noUncheckedIndexedAccess": true
  }
}
```

With this setting enabled, TypeScript infers `scores['bob']` as `number | undefined`. This forces you to check if the value exists before calling methods on it.

---

## Conclusion & Next Steps

The `Record` utility type brings clarity and safety to JavaScript objects:

1. **Enforce completeness**: Use `Record<MyUnion, ValueType>` so TypeScript forces you to handle every key.
2. **Combine with utilities**: Use `Partial<Record<K, T>>` when creating dynamic caches or lookup dictionaries.
3. **Turn on strict checks**: Use `noUncheckedIndexedAccess` in `tsconfig.json` to prevent runtime crashes on missing keys.

If you want to continue leveling up your TypeScript skills, explore these related articles on my blog:

- [Why Avoid Using 'any' in TypeScript](https://danywalls.com/why-avoid-using-any-in-typescript)
- [How to Create Type Definitions in TypeScript (.d.ts)](https://danywalls.com/how-to-create-type-definitions)
- [How to Check Types in TypeScript](https://danywalls.com/how-to-check-types-in-typescript)
- [3 Ways of Type Transformation in TypeScript](https://danywalls.com/3-ways-of-type-transformation-in-typescript)

