Question
Is it possible to use key-value pairs in TypeScript? If so, what is the recommended way to define, add, read, and type key-value data? Please provide practical examples.
Short Answer
TypeScript supports key-value data through plain objects, index signatures, the Record utility type, and JavaScript's Map. You will learn when to choose each option and how TypeScript helps ensure that keys and values have the expected types.
Concept
A key-value pair associates a unique identifier (the key) with a value.
For example, in this data:
const userRoles = {
alice: "admin",
bob: "editor"
};
aliceis a key and"admin"is its value.bobis a key and"editor"is its value.
TypeScript runs on JavaScript, so it uses JavaScript's built-in data structures. The most common choices are:
- Object: best for simple string-keyed data and JSON-like data.
Record<K, V>: a TypeScript type for an object whose keys and values follow specific rules.Map<K, V>: best when keys are dynamic, may not be strings, or when you need map-specific methods such as.set()and.has().
The important TypeScript addition is typing: it can verify that a dictionary contains valid values and, when possible, valid keys. This catches mistakes before the code runs.
Mental Model
Think of a key-value collection as a labelled storage cabinet:
- A key is the label on a drawer, such as
"theme". - A value is what is stored in that drawer, such as
"dark". - An object is a simple cabinet where drawer labels are usually text.
- A
Mapis a more flexible cabinet: labels can be text, numbers, or even objects.
TypeScript acts like an inventory rule. It can say, “Every drawer must contain a number,” or “Only these three drawer labels are allowed.”
Syntax and Examples
A plain object is the simplest key-value structure:
const prices: { [key: string]: number } = {
apple: 1.5,
banana: 0.75
};
prices.orange = 1.25;
console.log(prices.apple); // 1.5
console.log(prices["orange"]); // 1.25
{ [key: string]: number } is an index signature. It means:
- keys are strings
- every value is a number
The same type can be written more clearly with Record:
const scores: Record<string, number> = {
Maya: 98,
Leo: 87
};
scores.Ana = 92;
If the allowed keys are known in advance, use a union of literal keys:
type ThemeName = "light" | "dark";
const themeLabels: Record<ThemeName, string> = {
light: "Light mode",
dark: "Dark mode"
};
console.log(themeLabels.dark); // "Dark mode"
For a Map, declare both the key and value types:
const productStock = new Map<string, number>();
productStock.set("keyboard", 12);
productStock.set("mouse", 30);
console.log(productStock.get("keyboard")); // 12
console.log(productStock.has("monitor")); // false
Step by Step Execution
Consider this typed object:
const inventory: Record<string, number> = {
notebook: 10
};
inventory.pen = 25;
const remaining = inventory.notebook - 2;
console.log(remaining);
Execution proceeds as follows:
inventoryis created with one key-value pair:"notebook"→10.Record<string, number>tells TypeScript that any string key is allowed, but every stored value must be a number.inventory.pen = 25adds"pen"→25.inventory.notebookreads the value10.10 - 2produces8.- The program prints
8.
This would be rejected by TypeScript because the value is not a number:
inventory.eraser = "sold out";
Real World Use Cases
Key-value data appears throughout applications:
- Application settings:
Record<string, boolean>for feature flags such as"newCheckout"→true. - API response lookup: store users by ID for fast access, such as
usersById[userId]. - Shopping cart quantities: product IDs mapped to quantities.
- HTTP headers: header names mapped to header values.
- Translations: language keys mapped to display text, such as
"save"→"Save". - Caching: a
Mapcan associate request identifiers or object instances with computed results.
For data that will be converted to JSON or sent through an API, plain objects are usually the natural choice. Map is not directly represented by JSON without conversion.
Real Codebase Usage
In real projects, developers choose the structure based on the guarantees they need.
Validate unknown keys before using them
When a key comes from a user, URL, or API, it may not exist. Check it before relying on its value:
const statusMessages: Record<string, string> = {
pending: "Waiting for approval",
approved: "Approved"
};
function getStatusMessage(status: string): string {
return statusMessages[status] ?? "Unknown status";
}
The ?? operator provides a fallback if the value is undefined or null.
Use fixed keys for configuration
type Environment = "development" | "production";
const apiBaseUrls: Record<Environment, string> = {
development: "http://localhost:3000",
production: "https://api.example.com"
};
This guarantees that both environments are configured and prevents misspellings such as "prodution".
Use Map for non-string keys
type Session = { userId: string };
const sessionLastSeen = new Map<Session, Date>();
const session: Session = { userId: "u1" };
sessionLastSeen.set(session, new Date());
A plain object converts ordinary object keys to strings, while a Map preserves the original object reference as a key.
Common Mistakes
Assuming TypeScript creates a special key-value type at runtime
Record is only a TypeScript type. It does not create a new JavaScript collection. At runtime, this is still a normal object:
const flags: Record<string, boolean> = {};
Use Map when you specifically need runtime map behavior such as .set() and .get().
Using an array as a dictionary
This works at runtime but is misleading and weakly models the intent:
const values: string[] = [];
values["name"] = "Ada"; // Incorrect approach
Use an object or Map instead:
const values: Record<string, string> = {};
values.name = "Ada";
Forgetting that Map.get() can return undefined
const quantities = new Map<string, number>();
quantities.set("book", 3);
const quantity = quantities.get("pen"); // number | undefined
Handle the missing case:
const quantity = quantities.get("pen") ?? 0;
Treating arbitrary input as a known key
With a fixed-key Record, a string from external input is not automatically a safe key. Validate it or provide a fallback before use.
Using dot notation for keys that are not valid identifiers
const headers: Record<string, string> = {
"content-type": "application/json"
};
console.log(headers["content-type"]); // Correct
headers.content-type means subtraction, not access to the "content-type" key.
Comparisons
| Option | Best for | Key types | Reading a value | Notes |
|---|---|---|---|---|
| Plain object | JSON-like and simple data | Usually strings | obj[key] or obj.name | Most common choice |
| Index signature | Object with dynamic keys | string or number | obj[key] | Syntax: { [key: string]: Value } |
Record<K, V> | Clearly typed object dictionaries | Keys described by K | obj[key] | Readable alias for many dictionary types |
Map<K, V> | Dynamic data and non-string keys | Any type | map.get(key) | Has .set(), .has(), .delete(), and .size |
Record<string, number> and { [key: string]: number } often describe the same kind of object. Choose Record when it makes the key and value relationship easier to read.
Use an object when you want simple data or JSON serialization. Use a Map when key identity matters, when keys are objects, or when map methods are useful.
Cheat Sheet
// Dynamic string keys, numeric values
const counts: Record<string, number> = {};
counts.items = 3;
// Equivalent index signature
const counts2: { [key: string]: number } = {};
// Fixed set of required keys
type Role = "admin" | "member";
const permissions: Record<Role, boolean> = {
admin: true,
member: false
};
// Map with typed key and value
const cache = new Map<string, number>();
cache.set("answer", 42);
const value = cache.get("answer"); // number | undefined
- Use
Record<string, T>for a string-keyed object containingTvalues. - Use
Record<"a" | "b", T>when allowed keys are known. - Use bracket notation for dynamic keys:
object[key]. - Use
??to supply a default for a missing value. Map.get()may returnundefined.- Objects are easier to serialize as JSON;
Mapis more flexible at runtime.
FAQ
Does TypeScript have a dictionary type?
TypeScript does not have a separate built-in Dictionary keyword, but Record<string, T> and index signatures are standard ways to type dictionary-like objects.
Should I use Record or Map in TypeScript?
Use Record for plain, string-keyed object data. Use Map when you need non-string keys, .has(), .size, reliable insertion ordering APIs, or frequent additions and removals.
Can a TypeScript object have number keys?
Yes. You can use { [key: number]: string } or Record<number, string>. In ordinary JavaScript objects, property names are represented as strings at runtime, even when accessed with numbers.
How do I loop through key-value pairs in an object?
Use Object.entries():
const scores: Record<string, number> = { Ada: 10, Lin: 8 };
for (const [name, score] of Object.entries(scores)) {
console.log(name, score);
}
How do I loop through a Map?
const scores = new Map<string, number>([["Ada", 10]]);
for (const [name, score] of scores) {
console.log(name, score);
}
Can I store different value types in one key-value object?
Yes, with a union type, although a dedicated interface is often clearer when fields are known:
const data: Record<string, string | number | boolean> = {
name: "Ada",
age: 30,
active: true
};
Why does TypeScript say a value may be undefined?
A dynamic key might not be present. This is especially explicit with Map.get(). Handle the missing value with a check or a default such as value ?? fallback.
Mini Project
Description
Build a small shopping-cart quantity tracker. Product IDs are keys and the number of units selected is each value. This mirrors how applications often collect quantities before sending an order to an API.
Goal
Create typed functions that add products, read quantities safely, and calculate the total item count.
Requirements
Use a Record<string, number> to store product quantities.
Add at least two products to the cart.
Create a function that returns 0 for a product that is not in the cart.
Create a function that calculates the total number of items.
Do not allow non-numeric quantities in the cart.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.