How to Use the Record Type in TypeScript

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 bestring,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:
| 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.sizelookups.
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:
- Enforce completeness: Use
Record<MyUnion, ValueType>so TypeScript forces you to handle every key. - Combine with utilities: Use
Partial<Record<K, T>>when creating dynamic caches or lookup dictionaries. - Turn on strict checks: Use
noUncheckedIndexedAccessintsconfig.jsonto prevent runtime crashes on missing keys.
If you want to continue leveling up your TypeScript skills, explore these related articles on my blog:
Mastering the language of the web. From basic types to advanced generics and patterns.
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
How to Check Types in TypeScript (The Complete Guide)
Learn how to check and validate types in TypeScript at compile-time and runtime using typeof, instanceof, the in operator, discriminated unions, custom type guards, and Zod or Valibot.
3 Ways of Type Transformation in Typescript
When we use types or interfaces, the typescript compiler enforces the object fit with them to avoid runtime errors for missing fields. Sometimes we want the flexibility to create an object without breaking the contract with the interface type. For ex...
Why You Should Avoid Using 'any' in TypeScript
Why using any in TypeScript leads to hidden runtime bugs, and how to replace it with unknown, custom type guards, utility types, and the satisfies operator.
Combining Types and Interfaces with & and | in TypeScript
Learn how to compose TypeScript types using Union (|), Intersection (&), and interface extends with practical examples, decision matrices, and real-world patterns.
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.
Join 13,800+ developers and readers.
No spam ever. Unsubscribe at any time.