Record<K, V> is a TypeScript utility type that describes an object whose keys have the type K and whose values have the type V. For example, Record<string, number> describes an object such as { Lokesh: 37, Raj: 35 } that maps names to ages. A Record exists only at compile time, and the compiled JavaScript contains a plain object, so all object syntax and functions such as Object.keys() work on it.
We use a TypeScript Record in two ways.
- With a wide key type such as string, it is a dictionary whose keys are added at runtime.
- With a union of string literals as the key type, it is an object that must contain every key in the union, which makes the compiler report a missing or misspelled key.
The following example shows the daily operations on a Record, which use plain object syntax plus a few Object functions. It uses TypeScript 7.0.2 with strict enabled and Node.js 22.
const ages: Record<string, number> = { Lokesh: 37, Raj: 35 };
// 1. Read and add
const age = ages["Lokesh"]; // age = 37
ages["John"] = 40; // adds a key
// 2. Update and delete
ages["Raj"] = 36; // replaces 35
delete ages["John"];
// 3. Check if a key exists
const hasRaj = Object.hasOwn(ages, "Raj"); // hasRaj = true
const hasBrian = "Brian" in ages; // hasBrian = false
// 4. Count the keys
const count = Object.keys(ages).length; // count = 2
// 5. Iterate over keys and values
for (const [name, value] of Object.entries(ages)) {
console.log(name, value); // Lokesh 37, Raj 36
}
// 6. Fixed keys: every key is required
type Fruit = "apple" | "banana";
const stock: Record<Fruit, number> = { apple: 5, banana: 3 };
// 7. Optional keys
const some: Partial<Record<Fruit, number>> = { apple: 5 };
Notice that steps 6 and 7 use a union of keys, so stock must contain every fruit, whereas the Partial form in some may leave fruits out.
Next, we look at how Record is defined and at both forms in detail. After that, we answer the questions developers search for most, such as how to check if a key exists or how to iterate over a Record, and decide when a Map or an interface is the better choice.
1. How Record<K, V> Is Defined
Record is not a class but a type alias, which TypeScript’s standard library (lib.es5.d.ts) declares as a mapped type (a type that creates one property for each member of a key type).
type Record<K extends keyof any, T> = {
[P in K]: T;
};
The constraint K extends keyof any means the key type must be string, number, symbol, or a union of their literal types. The mapped type [P in K]: T reads as “for every key P in K, a property of type T“. So Record<“apple” | “banana”, number> expands to { apple: number; banana: number }, and Record<string, number> expands to the index signature { [key: string]: number }.
Because the result is an ordinary object type, a Record value works with JSON without a conversion, and JSON.stringify(ages) writes every key. TypeScript ships Record together with other built-in utility types, such as Partial.
2. Record<string, V> as a Dictionary
With string as the key type, the compiler allows any key, so keys can come and go at runtime. For example, a translation table maps message keys to text, and every release adds new keys. We read, add and update values with bracket or dot notation, and we remove a key with the delete operator.
const ages: Record<string, number> = {
Lokesh: 37,
Raj: 35,
};
// 1. Read with brackets or a dot
const a1 = ages["Lokesh"]; // a1 = 37
const a2 = ages.Raj; // a2 = 35
// 2. Add and update: assignment does both
ages["John"] = 40; // adds John
ages["Raj"] = 36; // replaces 35
// 3. Delete a key
delete ages["John"]; // ages = { Lokesh: 37, Raj: 36 }
// 4. A missing key is undefined, but the type says number
const missing = ages["Brian"]; // type number, value undefined
const safe = ages["Brian"] ?? 0; // safe = 0
Line 4 is the most common source of bugs with a Record<string, V>. TypeScript assumes that every string key has a value, so missing has the type number even though its value is undefined. A call such as ages[“Brian”].toFixed(1) compiles but throws a TypeError at runtime. We protect reads of a missing key in one of three ways.
- The ?? operator falls back to the right-hand value for null or undefined.
- A key check from section 4 tells us whether the key exists before we read it.
- The noUncheckedIndexedAccess compiler option changes the type of every index read to number | undefined.
The key type can also be number, and the value type can be anything, including arrays and objects.
// 1. Number keys; stored as strings at runtime
const squares: Record<number, number> = { 1: 1, 2: 4, 3: 9 };
const nine = squares[3]; // nine = 9
const keys = Object.keys(squares); // keys = ["1", "2", "3"]
// 2. Array or object values
const tags: Record<string, string[]> = { Lokesh: ["java", "ts"] };
const first = tags["Lokesh"][0]; // first = "java"
JavaScript object keys are always strings or symbols, so the number keys come back as strings from Object.keys(). When keys must keep their real type, for example numbers or objects, a Map is the better collection.
3. Record With a Union of Keys
When the key type is a union of string literals, the Record must have all of those keys and no others. The union form makes Record more than a shorter way to write an index signature. If someone adds “kiwi” to the union later, every Record typed with it fails to compile until the new key gets a value.
type Fruit = "apple" | "banana" | "mango";
// 1. Every key in the union is required
const stock: Record<Fruit, number> = {
apple: 5,
banana: 3,
mango: 0,
};
// 2. Partial: any subset of the keys
const sold: Partial<Record<Fruit, number>> = { apple: 2 };
const mangoSold = sold.mango ?? 0; // mangoSold = 0
// 3. Values can be objects
type Status = "active" | "blocked";
const labels: Record<Status, { text: string; color: string }> = {
active: { text: "Active", color: "green" },
blocked: { text: "Blocked", color: "red" },
};
const color = labels.blocked.color; // color = "red"
The compiler checks both directions, so it reports a missing key and an unknown key as errors.
type Fruit = "apple" | "banana" | "mango";
const s1: Record<Fruit, number> = { apple: 5, banana: 3 };
const s2: Record<Fruit, number> = { apple: 5, banana: 3, mango: 0, kiwi: 1 };
src/err.ts(2,7): error TS2741: Property 'mango' is missing in type '{ apple: number; banana: number; }' but required in type 'Record<Fruit, number>'.
src/err.ts(3,68): error TS2353: Object literal may only specify known properties, and 'kiwi' does not exist in type 'Record<Fruit, number>'.
Partial makes every property optional, so sold.mango has the type number | undefined and needs a default. We use the full Record for lookup tables that must cover every case, such as labels for every status, and the Partial form for data where some keys are often absent, such as counts. The result of Object.groupBy() is also typed as a Partial<Record<K, T[]>>, because a group with no items is left out. The key union can also come from an enum or from a literal type.
4. Checking if a Key Exists in a Record
A key check answers whether the object has the key, before we read the value. There are three ways, and they differ in how they treat keys inherited from Object.prototype, such as toString.
const ages: Record<string, number> = { Lokesh: 37, Raj: 35 };
// 1. Object.hasOwn(): own keys only (ES2022)
const h1 = Object.hasOwn(ages, "Raj"); // h1 = true
const h2 = Object.hasOwn(ages, "toString"); // h2 = false
// 2. The in operator also sees inherited keys
const i1 = "Raj" in ages; // i1 = true
const i2 = "toString" in ages; // i2 = true
// 3. Read the value and compare with undefined
const age = ages["Brian"];
const exists = age !== undefined; // exists = false
Object.hasOwn() is the safest check for keys that come from user input, because “toString” or “constructor” would make the in operator return true for an object that never stored them. It replaces ages.hasOwnProperty(“Raj”), which fails on objects created with Object.create(null) and can be shadowed by a key named hasOwnProperty. Option 3 needs a single lookup and is enough when no stored value can be undefined.
5. Iterating Over a Record
To iterate over a Record, we use the standard Object functions.
- Object.entries() returns [key, value] pairs.
- Object.keys() returns the keys.
- Object.values() returns the values.
A for…in loop also works, but it includes inherited enumerable keys, so it needs an Object.hasOwn() check.
const ages: Record<string, number> = { Lokesh: 37, Raj: 35, John: 40 };
// 1. Keys and values together
for (const [name, age] of Object.entries(ages)) {
console.log(name, age); // Lokesh 37, Raj 35, John 40
}
// 2. Only keys or only values
const names = Object.keys(ages); // names = ["Lokesh", "Raj", "John"]
const total = Object.values(ages).reduce((s, a) => s + a, 0); // total = 112
// 3. for...in visits keys, including inherited enumerable ones
for (const name in ages) {
if (Object.hasOwn(ages, name)) {
console.log(name, ages[name]); // Lokesh 37, Raj 35, John 40
}
}
With union keys, one detail causes a compile error. Object.keys() always returns string[], even for a Record<Fruit, number>, because TypeScript object types are open, so a value of that type may hold extra keys at runtime. Using a plain string key to index the Record fails under strict mode.
type Fruit = "apple" | "banana";
const stock: Record<Fruit, number> = { apple: 5, banana: 3 };
for (const fruit of Object.keys(stock)) {
console.log(stock[fruit]);
}
src/err.ts(4,15): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<Fruit, number>'.
No index signature with a parameter of type 'string' was found on type 'Record<Fruit, number>'.
There are two fixes.
- When we created the object ourselves and know it has no extra keys, a type assertion on the key array is acceptable.
- Otherwise, Object.entries() gives the value together with the key, so no indexing is needed.
// 1. Object.keys() returns string[], so cast to the key type
for (const fruit of Object.keys(stock) as Fruit[]) {
console.log(fruit, stock[fruit]); // apple 5, banana 3
}
// 2. Object.entries() needs no cast for the value
for (const [fruit, count] of Object.entries(stock)) {
console.log(fruit, count); // apple 5, banana 3
}
The iteration order follows the object rules, not insertion order alone. Integer-like keys are listed first, sorted as numbers, followed by the other string keys in insertion order. For example, Object.keys({ b: 1, 2: 1, a: 1, 1: 1 }) returns [“1”, “2”, “b”, “a”]. A Map keeps pure insertion order for every key type.
6. Transforming a Record
A Record has no map() or filter() method. We convert it to an array of pairs with Object.entries(), change the array, and turn it back into an object with Object.fromEntries(). Building a Record in a loop works for counting and grouping.
const stock: Record<string, number> = { apple: 5, banana: 3, mango: 0 };
// 1. Change every value
const doubled = Object.fromEntries(
Object.entries(stock).map(([fruit, n]) => [fruit, n * 2]),
); // doubled = { apple: 10, banana: 6, mango: 0 }
// 2. Keep only some keys
const inStock = Object.fromEntries(
Object.entries(stock).filter(([, n]) => n > 0),
); // inStock = { apple: 5, banana: 3 }
// 3. Build a Record from an array: count words
const counts: Record<string, number> = {};
for (const word of ["apple", "kiwi", "apple"]) {
counts[word] = (counts[word] ?? 0) + 1;
} // counts = { apple: 2, kiwi: 1 }
The result of Object.fromEntries() is typed as { [k: string]: number }, which is the same shape as Record<string, number>. In option 2, the empty slot in [, n] skips the key and keeps only the value. In option 3, counts[word] ?? 0 returns 0 the first time a word appears.
7. Annotation vs satisfies
Writing const prices: Record<string, number> = {…} gives the variable the wide type, so the compiler forgets which keys the object has and a misspelled key compiles. The satisfies operator, added in TypeScript 4.9, checks the object against the Record type but keeps the object’s own, exact type.
// 1. Annotation: the variable type is Record<string, number>
const prices: Record<string, number> = { apple: 5, banana: 3 };
const typo = prices.aple; // compiles, value undefined
// 2. satisfies: values are checked, the exact keys are kept
const prices2 = { apple: 5, banana: 3 } satisfies Record<string, number>;
const apple = prices2.apple; // apple = 5
With satisfies, the compiler catches both a wrong value and a misspelled key.
const prices = { apple: 5, banana: 3 } satisfies Record<string, number>;
const typo = prices.aple;
const bad = { apple: 5, banana: "3" } satisfies Record<string, number>;
src/err.ts(2,21): error TS2551: Property 'aple' does not exist on type '{ apple: number; banana: number; }'. Did you mean 'apple'?
src/err.ts(3,25): error TS2322: Type 'string' is not assignable to type 'number'.
We use satisfies for constant lookup tables written in the source code, such as prices or labels. We use the annotation when the object is a dictionary that gains keys at runtime, because a satisfies object cannot get new keys later.
8. Record, Index Signature, interface or Map
TypeScript offers several ways to type key-value data, and they overlap. The type Record<string, V> and the index signature { [key: string]: V } are the same type; the index signature lets us name the key, which shows up in editor hints. An interface fits objects whose properties have different value types.
| Need | Best fit |
|---|---|
| Fixed set of keys, same value type | Record<“a” | “b”, V> |
| Some keys of a fixed set | Partial<Record<K, V>> |
| Keys added at runtime, string keys, JSON data | Record<string, V> or an index signature |
| Properties with different types (name: string, age: number) | An interface or a type alias |
| Non-string keys, frequent adds and deletes, insertion order | Map<K, V> |
Performance and JSON serialization also matter when we choose between a Record and a Map, because a Map handles frequent changes better but needs a conversion for JSON.
9. Running the Example Code
In the typescript-record folder, each section has a source file, and npm start executes them one after another. Object.hasOwn() comes from ES2022, so the ES2024 library set in tsconfig.json covers it; the project pins TypeScript 7.0.2 and expects Node.js 22 or later.
npm install
npm start
Each section prints what its snippet comments promise, for example typo = undefined apple = 5 for section 7.
10. Conclusion
Record<K, V> types an object by its key and value types. With string keys, it is a dictionary; we add keys by assignment, check them with Object.hasOwn(), iterate with Object.entries(), and guard reads with ?? because a missing key returns undefined. With a union of keys, it is a complete lookup table that the compiler keeps in sync with the union, and Partial relaxes that when keys are optional. The satisfies operator keeps exact keys for constant tables. For non-string keys or data that changes often, a Map fits better.
11. References
The TypeScript Handbook and release notes describe Record, satisfies and the compiler options used here; MDN covers the Object functions.
- TypeScript Handbook: Utility types, Record
- TypeScript Handbook: Mapped types
- TypeScript 4.9 release notes: The satisfies operator
- TSConfig reference: noUncheckedIndexedAccess
- MDN: Object.hasOwn()
- MDN: Object.entries()
- MDN: Object.fromEntries()
Happy Learning !!