Question

TypeScript Dictionary Objects: How to Type Key-Value Objects

typescriptarraysobjectdictionary

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 PersonData value.”

That makes the structure predictable and safer to use.

Take Quiz

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

  • people is 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:

  1. interface PersonData defines the shape of each stored value.

    • Every person must have a name
    • Every person must have an age
  2. const people: Record<string, PersonData> = {};

    • creates an empty object
    • tells TypeScript that string keys will map to PersonData values
  3. people["alice@example.com"] = ...

    • adds a new entry using the email as the key
    • the value must match the PersonData shape
  4. people["bob@example.com"] = ...

    • adds another entry
  5. const person = people["alice@example.com"];

    • reads the value stored under that email key
  6. if (person) { ... }

    • checks that a value exists before using it
    • this avoids runtime errors if the key is missing
  7. 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 true or false values keyed by flag name
  • Translations by language key
    • map keys like "welcome_message" to translated text
  • 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);
Take Quiz

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:

  • Object is 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, person may be undefined
  • accessing .name may 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:

  • age is 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 number values, then name must also be a number

TypeScript enforces consistency here.

Comparisons

Here is how dictionary objects compare to related options.

OptionBest forKey typeNotes
Record<string, T>Simple key-value objectsstringClean and readable for dictionaries
Index signatureCustom dictionary typesstring, number, symbolMore flexible when defining interfaces
Array T[]Ordered listsnumberNot suitable for email or named keys
Map<K, V>Advanced key-value storageany typeUseful 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 Record for 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");
Take Quiz

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
  • Object is 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more