12.4 Record<K, V>: Typed Dictionaries
Let’s be honest: sometimes you just need a good, old-fashioned dictionary. Not a Map, with its fancy object keys and runtime methods—I mean a plain, string-keyed object holding a bunch of values. You’ve probably been typing these manually:
type User = {
id: string;
name: string;
};
const userLookup: { [key: string]: User } = {
'abc123': { id: 'abc123', name: 'Alice' },
'def456': { id: 'def456', name: 'Bob' },
};
This works, but it’s a bit… pedestrian. It also leaves a tiny crack in your type safety. What if you want to enforce that the keys are the id of the User? Good luck doing that cleanly with an index signature. This is where Record<K, V> waltzes in, wearing a perfectly tailored suit.
Record<K, V> is a utility type that constructs an object type whose property keys are of type K and whose property values are of type V. It’s the TypeScript-approved way of saying, “I want a dictionary, please.”
The Basic Syntax
The syntax is brutally straightforward. You feed it two types: one for the keys (K) and one for the values (V).
const userLookup: Record<string, User> = {
'abc123': { id: 'abc123', name: 'Alice' },
'def456': { id: 'def456', name: 'Bob' },
};
This is functionally identical to our manual { [key: string]: User } but with less typing and more semantic clarity. You’re explicitly declaring, “This is a record of User objects.”
Why Not Just Use an Index Signature?
Good question. For string keys, the difference is minimal. But Record truly shines when you move beyond simple strings. The K in Record<K, V> can be a union of string literal types. This is its killer feature.
Let’s say your application only has three valid user roles. You can use a Record to create an object that must have exactly these keys.
type UserRole = 'admin' | 'editor' | 'subscriber';
type RolePermissions = Record<UserRole, boolean>;
const permissions: RolePermissions = {
admin: true,
editor: true,
subscriber: false
};
// This is valid.
const badPermissions: RolePermissions = {
admin: true,
};
// Error: Property 'editor' is missing.
// Error: Property 'subscriber' is missing.
Try doing that succinctly with a simple index signature. You can’t. An index signature { [key: string]: boolean } implies you can have any string key, not that you must have a specific set. Record<'admin' | 'editor' | 'subscriber', boolean> is fundamentally different and infinitely more precise.
The keyof Tango
Record becomes an absolute powerhouse when combined with keyof. Need a type that mirrors the structure of an existing type but changes all its value types? This is a one-liner.
interface ServerConfig {
host: string;
port: number;
apiPrefix: string;
}
// Let's make a type for a configuration form, where every field might have an error message.
type ConfigValidation = Record<keyof ServerConfig, string | undefined>;
const validationState: ConfigValidation = {
host: "Must be a valid URL",
port: undefined, // No error for port... yet.
apiPrefix: "Cannot be empty"
};
It’s elegant, DRY, and automatically stays in sync if ServerConfig changes. This pattern is ridiculously useful for state management, validation, and configuration.
Common Pitfalls and The string Constraint
Here’s the part where I have to be the bearer of bad news. The TypeScript designers, in their infinite wisdom, decided that the key type K must extend string | number | symbol. This makes sense—object keys can only be those primitives. But it leads to a classic “wait, what?” moment.
You cannot use just any old type as K. If you have a union of complex objects and try to use it in a Record, you’ll get a type error. The key must be a type that can actually serve as a key.
type ComplexKey = { id: number }; // Nope, not allowed.
// Type 'ComplexKey' does not satisfy the constraint 'string | number | symbol'.
This isn’t a limitation of Record; it’s a limitation of JavaScript objects themselves. If you find yourself needing this, you probably want a Map.
Another gotcha is with optional properties. A Record<string, V> does not imply that every possible string key must exist. It means that if a key exists, its value must be of type V. This is a crucial distinction. The object can be empty. To enforce the presence of all keys, you must use a union of literals as shown in the RolePermissions example.