TypeScript
·6 min read·↗

How to Use the Record Type in TypeScript

Main cover illustration for article: How to Use the Record Type in TypeScript
Summarize with AI:

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:

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:

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:

type TeamName = 'Lakers' | 'Warriors' | 'Bulls' | 'Celtics';
 
// Construct the Record type
type ChampionshipRecord = Record<TeamName, number>;

Now we define our object using ChampionshipRecord:

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:

// 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:

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):

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:

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:

FeatureRecord<K, T>Index Signature ({ [k: string]: T })JavaScript Map<K, V>
Supported Key Typesstring, number, symbol, or unionsstring, number, symbol (no unions)Any type (objects, functions, primitives)
Exhaustive Key CheckYes (when using a union of keys)No (allows any arbitrary key)No
JSON SerializationNative (JSON.stringify)Native (JSON.stringify)Requires custom conversion
Iteration OrderObject property orderObject property orderGuaranteed insertion order
Performance with Frequent DeletionsModerateModerateHigh (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:

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:

{
  "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:

Part of the TypeScript Series

Mastering the language of the web. From basic types to advanced generics and patterns.

View Entire Series

Frequently Asked Questions

What is the Record utility type in TypeScript?

The Record<K, T> utility type constructs an object type whose property keys are of type K and whose property values are of type T.

What is the difference between Record and an index signature?

An index signature {[key: string]: T} allows any arbitrary string as a key. A Record allows you to constrain keys to specific string literal unions (like 'admin' | 'guest'), ensuring exhaustiveness.

When should I choose Map over Record in TypeScript?

Use Record for static dictionaries, configuration objects, and JSON data. Use Map when you need frequent insertions and deletions, non-string keys, or need to preserve insertion order.

Why does TypeScript allow accessing undefined keys in a Record?

By default, TypeScript assumes every key defined in Record<string, T> exists. To force TypeScript to include undefined in property lookups, enable noUncheckedIndexedAccess in your tsconfig.json.

Related Articles

Share this article

If you found this guide helpful, consider sharing it with your team or fellow developers.


Real Software. Real Lessons.

I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.

User avatar
User avatar
User avatar
User avatar
+13K

Join 13,800+ developers and readers.

No spam ever. Unsubscribe at any time.