Question
How can I loop through the keys of a string-indexed dictionary in TypeScript? For example, given this dictionary type and value, how do I access each key (and optionally each value)?
const myDictionary: { [index: string]: any } = {};
Short Answer
You will learn how TypeScript represents string-keyed dictionaries and how to iterate through their keys, values, and key-value pairs. You will also see why Record<string, unknown> is usually safer than using any.
Concept
A dictionary is an object that stores values under named keys. In TypeScript, a string index signature describes an object whose property names are strings:
const scores: { [key: string]: number } = {
alice: 10,
bob: 20
};
The type means: “for every string key in this object, the associated value is a number.”
At runtime, dictionaries are ordinary JavaScript objects. Therefore, you can use standard object APIs to inspect them:
Object.keys(object)returns an array of keys.Object.values(object)returns an array of values.Object.entries(object)returns key-value pairs.for...invisits enumerable property names.
The important TypeScript part is choosing an accurate value type. Avoid any when possible because it disables type checking. If values may have different, unknown shapes, use unknown instead.
Mental Model
Think of a dictionary as a labelled storage cabinet.
- Each key is a label on a drawer, such as
"alice"or"theme". - Each value is the item inside that drawer.
Object.keys()gives you a list of drawer labels.Object.values()gives you the contents.Object.entries()gives you both each label and its content together.
The index signature tells TypeScript what kind of item every drawer is allowed to contain.
Syntax and Examples
Use Object.keys() when you only need dictionary keys.
const myDictionary: Record<string, string> = {
firstName: "Ada",
lastName: "Lovelace",
role: "Programmer"
};
for (const key of Object.keys(myDictionary)) {
console.log(key);
}
Output:
firstName
lastName
role
Access the value for a key with bracket notation:
for (const key of Object.keys(myDictionary)) {
const value = myDictionary[key];
console.log(`${key}: ${value}`);
}
When both the key and value are needed, Object.entries() is often clearer:
for (const [key, value] of Object.entries(myDictionary)) {
console.log(`${key}: ${value}`);
}
A generic helper can preserve the dictionary value type:
function printDictionary<T>(dictionary: Record<string, T>): void {
for (const [key, value] of Object.entries(dictionary)) {
console.log(key, value);
}
}
printDictionary({ apples: 3, oranges: 5 });
Step by Step Execution
Consider this code:
const stock: Record<string, number> = {
apples: 8,
bananas: 3
};
for (const key of Object.keys(stock)) {
const quantity = stock[key];
console.log(`${key} has ${quantity} items`);
}
Execution proceeds as follows:
stockis created with two string keys:"apples"and"bananas".Object.keys(stock)produces the array["apples", "bananas"].- The first loop iteration assigns
"apples"tokey. stock[key]becomesstock["apples"], which is8.- The first message is printed:
apples has 8 items. - The next iteration assigns
"bananas"tokey. stock[key]becomes3, and the second message is printed.
Bracket notation is required because key is a variable. stock.key would look for a literal property named "key".
Real World Use Cases
String-keyed dictionaries appear frequently in applications:
- HTTP headers:
Record<string, string>for header names and values. - Configuration settings: feature flags such as
{ darkMode: true, betaSearch: false }. - Inventory data: product IDs mapped to quantities.
- Form validation: field names mapped to error messages.
- Translation tables: language keys mapped to translated text.
- API response metadata: dynamic names mapped to values.
Example: rendering validation errors in a UI:
const errors: Record<string, string> = {
email: "Enter a valid email address",
password: "Password must contain at least 12 characters"
};
for (const [field, message] of Object.entries(errors)) {
console.log(`${field}: ${message}`);
}
Real Codebase Usage
In production code, developers generally use Record<string, T> for simple dictionaries and choose a precise T value type.
Validate unknown data before using it
If data comes from JSON, an API, or user input, start with unknown and validate values before treating them as a specific type:
const settings: Record<string, unknown> = {
retries: 3,
debug: true
};
for (const [key, value] of Object.entries(settings)) {
if (typeof value === "number") {
console.log(`${key} is numeric: ${value}`);
}
}
Use guard clauses while iterating
const labels: Record<string, string | undefined> = {
save: "Save",
cancel: undefined
};
for (const [key, label] of Object.entries(labels)) {
if (label === undefined) {
continue;
}
console.log(`Rendering ${key}: ${label}`);
}
Use Object.entries() for transformations
A common pattern is filtering a dictionary and rebuilding it:
const quantities: Record<string, number> = {
apples: 4,
bananas: 0,
pears: 2
};
const available = Object.fromEntries(
Object.entries(quantities).filter(([, quantity]) => quantity > 0)
);
console.log(available); // { apples: 4, pears: 2 }
Common Mistakes
Using for...of directly on an object
Objects are not iterable by default, so this does not work:
const dictionary: Record<string, number> = { apples: 2 };
// TypeError at runtime / TypeScript error
for (const key of dictionary) {
console.log(key);
}
Use Object.keys(dictionary) instead.
Using dot notation with a variable key
const dictionary: Record<string, number> = { apples: 2 };
const key = "apples";
console.log(dictionary.key); // Looks for a property literally named "key"
Use bracket notation:
console.log(dictionary[key]); // 2
Using any unnecessarily
const data: { [index: string]: any } = {};
any lets you call invalid methods without a TypeScript error. Prefer a specific type:
const data: Record<string, number> = {};
If the values are truly unknown, use:
const data: Record<string, unknown> = {};
Forgetting inherited properties with for...in
for...in can include enumerable properties inherited from an object's prototype. For ordinary dictionaries, Object.keys() is usually the safer and clearer default. If you use for...in, check ownership:
for (const key in dictionary) {
if (Object.prototype.hasOwnProperty.call(dictionary, key)) {
console.log(key);
}
}
Assuming a key always exists
A string index signature may imply a value exists, but real data can be incomplete. When missing values are possible, represent that in the type:
const labels: Record<string, string | undefined> = {};
const label = labels["save"];
if (label !== undefined) {
console.log(label.toUpperCase());
}
Comparisons
| Approach | Produces | Best use | Notes |
|---|---|---|---|
Object.keys(obj) | string[] | You need keys | Look up each value with obj[key]. |
Object.values(obj) | Values array | You only need values | Keys are not available in the loop. |
Object.entries(obj) | [key, value] pairs | You need both | Usually the most readable option for both. |
for...in | Property names | Legacy or special object iteration | Check own properties when needed. |
Map<string, T> | Iterable key-value pairs | Frequent additions, removals, or non-string keys | A Map has methods such as .get(), .set(), and .has(). |
{ [key: string]: T } and Record<string, T> both describe string-keyed objects in many common cases:
const a: { [key: string]: number } = { apples: 2 };
const b: Record<string, number> = { apples: 2 };
Record<string, number> is often shorter and easier to read. An index signature can be more flexible when declared alongside named properties in an interface.
Cheat Sheet
// A string-keyed dictionary
const counts: Record<string, number> = {
apples: 2,
pears: 5
};
// Keys
for (const key of Object.keys(counts)) {
console.log(key);
}
// Values
for (const value of Object.values(counts)) {
console.log(value);
}
// Keys and values
for (const [key, value] of Object.entries(counts)) {
console.log(key, value);
}
// Dynamic key access: use brackets
const key = "apples";
console.log(counts[key]);
- Prefer
Record<string, T>for a dictionary with string keys. - Use the most specific possible
T, such asstring,number, orUser. - Prefer
unknownoveranyfor unvalidated values. - Use
Object.keys()for keys andObject.entries()for key-value pairs. - Do not use
for...ofdirectly on a plain object. - Use
for...incarefully because it can visit inherited enumerable properties.
FAQ
How do I loop through the keys of an object in TypeScript?
Use Object.keys() with for...of:
for (const key of Object.keys(object)) {
console.log(key);
}
How do I get both keys and values from a TypeScript dictionary?
Use Object.entries():
for (const [key, value] of Object.entries(dictionary)) {
console.log(key, value);
}
Should I use { [key: string]: any } for a dictionary?
Usually no. Prefer Record<string, T> with a known value type. Use Record<string, unknown> when values are not yet known or validated.
Why does dictionary.key not work in a loop?
Dot notation searches for a literal property named key. For the value stored in the key variable, use dictionary[key].
Is for...in safe for iterating over object keys?
It can be, but it may include inherited enumerable properties. Object.keys() returns only the object's own enumerable keys and is usually preferred.
When should I use a Map instead of an object dictionary?
Use Map when you need built-in methods such as .has(), frequent updates, guaranteed iteration APIs, or keys that are not strings. Use an object for simple string-keyed data structures.
Does Object.keys() preserve TypeScript literal key types?
For a general object, TypeScript commonly types the result as string[]. This is appropriate for a dynamic string-indexed dictionary. For fixed object shapes, extra typing may be useful, but dynamic dictionaries should normally be handled as string-keyed data.
Mini Project
Description
Build a small inventory report from a string-keyed dictionary. This reflects a common backend or scripting task: process dynamic product IDs and show only items that need attention.
Goal
Print an inventory report that lists each product and identifies products with low stock.
Requirements
Create a Record<string, number> containing at least four product quantities.
Loop through every product key and quantity.
Print each product name and its quantity.
Print an additional low-stock warning for quantities below 5.
Do not use any.
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.