Question
TypeScript Dictionary Objects: How to Type Key-Value Objects
Question
I have JavaScript code that uses plain objects as dictionaries. For example, a people object stores personal details, keyed by email address.
var people = {
// email: personal data
};
people[email] = data; // add or update
var data = people[email]; // read
delete people[email]; // remove
Is it possible to describe this pattern in TypeScript, or do I need to use an array instead?
Short Answer
By the end of this page, you will understand how to type dictionary-style objects in TypeScript using index signatures and Record. You will also learn when to use an object, when an array is the wrong choice, and what common mistakes to avoid when storing values by dynamic string keys such as email addresses.
Concept
In JavaScript, objects are often used as dictionaries: collections of key-value pairs where you look up a value by a dynamic key.
In your example, the key is an email address and the value is some person-related data.
TypeScript supports this pattern directly. You do not need to use an array for this. In fact, an array would usually be the wrong tool unless your keys are numeric indexes.
The main TypeScript feature for this is an index signature.
interface PeopleDictionary {
[email: string]: PersonData;
}
This means:
- the object can have any number of properties
- each property key is a
string - each property value must be of type
PersonData
This is useful because it lets TypeScript check that:
- you only store the right kind of value
- you access the dictionary in a type-safe way
- your code communicates intent clearly
A more modern shorthand is Record<string, PersonData>:
type PeopleDictionary = Record<string, PersonData>;
Both approaches describe a dictionary-like object.
Why this matters in real programming:
- caching API results by ID or email
- storing settings by name
- grouping data by category
- building lookup tables for fast access
- tracking users, sessions, or permissions by key
Mental Model
Think of a dictionary object like a set of mailboxes.
- The label on each mailbox is the key, such as an email address.
- The content inside the mailbox is the value, such as a person's data.
TypeScript lets you say:
- what kind of labels are allowed
- what kind of content each mailbox can contain
So instead of saying “this is just some object,” you say:
“This is an object where every string key points to a
PersonDatavalue.”
That makes the structure predictable and safer to use.
Syntax and Examples
The most common ways to type a dictionary in TypeScript are index signatures and Record.
Using an index signature
interface PersonData {
name: string;
age: number;
}
interface PeopleDictionary {
[email: string]: PersonData;
}
const people: PeopleDictionary = {};
people["alice@example.com"] = { name: "Alice", age: 30 };
people["bob@example.com"] = { name: "Bob", age: 25 };
const person = people["alice@example.com"];
console.log(person.name); // Alice
delete people["bob@example.com"];
What this means
peopleis an object- any string can be used as a key
- every value must match
PersonData
Using Record
interface PersonData {
name: string;
age: number;
}
const people: Record<string, PersonData> = {};
people["alice@example.com"] = { name: "Alice", age: 30 };
Record<string, PersonData> means the same thing as “an object whose string keys map to PersonData values.”
If a lookup may be missing
In real code, a key might not exist.
interface PersonData {
name: string;
age: number;
}
const people: Record<string, PersonData> = {};
const person = people["missing@example.com"];
Depending on your TypeScript settings, this may need to be treated carefully because the result might be undefined at runtime.
A safer version is:
const person = people["missing@example.com"];
if (person) {
console.log(person.name);
} else {
console.log("Person not found");
}
Array vs dictionary
Use an array when:
- order matters
- you access items by position
- keys are numeric indexes like
0,1,2
Use an object dictionary when:
- you access items by a named key
- keys are strings like emails, IDs, or usernames
- you want fast lookups by key
Step by Step Execution
Consider this example:
interface PersonData {
name: string;
age: number;
}
const people: Record<string, PersonData> = {};
people["alice@example.com"] = { name: "Alice", age: 30 };
people["bob@example.com"] = { name: "Bob", age: 25 };
const person = people["alice@example.com"];
if (person) {
console.log(person.name);
}
delete people["bob@example.com"];
Here is what happens step by step:
-
interface PersonDatadefines the shape of each stored value.- Every person must have a
name - Every person must have an
age
- Every person must have a
-
const people: Record<string, PersonData> = {};- creates an empty object
- tells TypeScript that string keys will map to
PersonDatavalues
-
people["alice@example.com"] = ...- adds a new entry using the email as the key
- the value must match the
PersonDatashape
-
people["bob@example.com"] = ...- adds another entry
-
const person = people["alice@example.com"];- reads the value stored under that email key
-
if (person) { ... }- checks that a value exists before using it
- this avoids runtime errors if the key is missing
-
delete people["bob@example.com"];- removes that entry from the object
After deletion, looking up people["bob@example.com"] returns undefined at runtime.
Real World Use Cases
Dictionary-style objects appear everywhere in real applications.
Common use cases
- Users by email
- store user profiles keyed by email address
- Products by ID
- quickly look up product data without looping through a list
- Feature flags by name
- store
trueorfalsevalues keyed by flag name
- store
- Translations by language key
- map keys like
"welcome_message"to translated text
- map keys like
- Cached API responses
- store responses by URL or resource ID
- Session data
- track session information by token or session ID
Example: feature flags
const flags: Record<string, boolean> = {
darkMode: true,
betaDashboard: false
};
if (flags["darkMode"]) {
console.log("Enable dark theme");
}
Example: product lookup
interface Product {
id: string;
name: string;
price: number;
}
const productsById: Record<string, Product> = {};
productsById["p101"] = { id: "p101", name: "Keyboard", price: 49 };
productsById["p102"] = { id: "p102", name: "Mouse", price: 25 };
console.log(productsById["p101"].name);
Real Codebase Usage
In real projects, developers often use typed dictionaries for fast lookups, caching, validation results, and grouped data.
Common patterns
1. Lookup tables
Instead of searching an array repeatedly, code converts data into an object keyed by ID.
interface User {
id: string;
name: string;
}
const usersById: Record<string, User> = {};
This makes access simple:
const user = usersById[userId];
2. Guard clauses for missing entries
const user = usersById[userId];
if (!user) {
throw new Error("User not found");
}
console.log(user.name);
This is common in services, controllers, and business logic.
3. Validation error collections
const errors: Record<string, string> = {};
errors.email = "Email is required";
errors.password = "Password must be at least 8 characters";
This pattern is common in forms and APIs.
4. Grouping values
const ordersByStatus: Record<string, number[]> = {};
Each key points to a list of related items.
5. Configuration maps
const config: Record<string, string | number | boolean> = {
appName: "My App",
port: 3000,
debug: true
};
Practical advice
In larger codebases:
- use a named type for readability
- check for missing keys before using results
- prefer
Record<string, T>for simple cases - use interfaces when you want a more descriptive custom type name
Common Mistakes
Here are some common mistakes beginners make when typing dictionary objects.
1. Using an array for string keys
Broken code:
const people: PersonData[] = [];
people["alice@example.com"] = { name: "Alice", age: 30 };
Why this is a problem:
- arrays are meant for numeric indexes
- string keys on arrays are confusing and not the intended pattern
Use this instead:
const people: Record<string, PersonData> = {};
2. Using Object as the type
Broken code:
const people: Object = {};
people["alice@example.com"] = { name: "Alice", age: 30 };
Why this is a problem:
Objectis too vague- TypeScript cannot properly check the value shape
Use a typed dictionary instead.
3. Forgetting that a key may not exist
Broken code:
const person = people["missing@example.com"];
console.log(person.name);
Why this is a problem:
- at runtime,
personmay beundefined - accessing
.namemay crash
Safer version:
const person = people["missing@example.com"];
if (person) {
console.log(person.name);
}
4. Storing values that do not match the type
Broken code:
interface PersonData {
name: string;
age: number;
}
const people: Record<string, PersonData> = {};
people["alice@example.com"] = { name: "Alice" };
Why this is a problem:
ageis required but missing
Correct version:
people["alice@example.com"] = { name: "Alice", age: 30 };
5. Mixing fixed properties and incompatible index signatures
Broken code:
interface BadExample {
[key: string]: number;
name: string;
}
Why this is a problem:
- if all string keys must have
numbervalues, thennamemust also be anumber
TypeScript enforces consistency here.
Comparisons
Here is how dictionary objects compare to related options.
| Option | Best for | Key type | Notes |
|---|---|---|---|
Record<string, T> | Simple key-value objects | string | Clean and readable for dictionaries |
| Index signature | Custom dictionary types | string, number, symbol | More flexible when defining interfaces |
Array T[] | Ordered lists | number | Not suitable for email or named keys |
Map<K, V> | Advanced key-value storage | any type | Useful when keys are not just strings or when map-specific methods are needed |
Record vs index signature
type A = Record<string, PersonData>;
interface B {
[key: string]: PersonData;
}
These are very similar for this use case.
- Use
Recordfor short, simple definitions - Use an index signature when defining a custom interface feels clearer
Object dictionary vs array
const peopleByEmail: Record<string, PersonData> = {};
const peopleList: PersonData[] = [];
Choose the dictionary when you need:
- lookup by email
- lookup by ID
- grouping by name or category
Choose an array when you need:
- iteration in order
- list rendering
- methods like
.map(),.filter(),.find()
Object dictionary vs Map
Map can be better if:
- keys are objects
- insertion order matters in a specific way
- you want methods like
.set(),.get(),.has(),.delete()
Example:
const people = new Map<string, PersonData>();
people.set("alice@example.com", { name: "Alice", age: 30 });
const person = people.get("alice@example.com");
Cheat Sheet
Quick syntax
Index signature
interface Dictionary {
[key: string]: ValueType;
}
Record
type Dictionary = Record<string, ValueType>;
Example
interface PersonData {
name: string;
age: number;
}
const people: Record<string, PersonData> = {};
people["alice@example.com"] = { name: "Alice", age: 30 };
const person = people["alice@example.com"];
delete people["alice@example.com"];
Use a dictionary when
- keys are strings
- you need fast lookup by key
- values all have the same general type
Use an array when
- items are ordered
- you access by numeric index
- you want list operations like
.map()and.filter()
Watch out for
- missing keys may produce
undefined - arrays are not for email-based lookup
Objectis too broad as a type- all values must match the declared value type
Common patterns
Record<string, string>
Record<string, number>
Record<string, boolean>
Record<string, PersonData>
Record<string, PersonData[]>
Delete syntax
delete people[email];
FAQ
Can I use an object as a dictionary in TypeScript?
Yes. This is a common pattern. Use an index signature or Record<string, T> to type it safely.
Do I need an array instead of an object?
No. Use an array only when your data is a list accessed by numeric index. For email or ID keys, an object dictionary is usually better.
What is the TypeScript type for dynamic string keys?
A common choice is:
Record<string, T>
or
interface MyDictionary {
[key: string]: T;
}
What happens if the key does not exist?
At runtime, the result is undefined. Check that the value exists before using its properties.
Should I use Record or an interface with an index signature?
Both work well. Record is shorter. An interface can be clearer when you want a named custom type.
Is Map better than an object in TypeScript?
Sometimes. Use Map when you need special map methods or non-string keys. For simple string-key dictionaries, an object is often enough.
Can I delete keys from a typed dictionary?
Yes.
delete people[email];
This removes the property from the object.
Mini Project
Description
Build a small email-based contact lookup. This project demonstrates how to use a typed dictionary to store, retrieve, update, and delete person records by email address. It reflects a common real-world pattern used in user directories, caches, and in-memory lookup tables.
Goal
Create a TypeScript dictionary that stores contacts by email and provides basic add, read, update, and delete operations.
Requirements
- Define a type for contact data with at least a name and phone number.
- Create a dictionary object keyed by email address.
- Add at least three contacts to the dictionary.
- Read and print one contact by email.
- Delete one contact and show that it no longer exists.
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.