Question
I want to store a mapping of string -> string in a TypeScript object and make sure every assigned value is a string.
For example:
const stuff = {};
stuff["a"] = "foo"; // okay
stuff["b"] = "bar"; // okay
stuff["c"] = false; // should be an error because boolean is not a string
Is there a way to define the object so TypeScript enforces that all indexed values must be strings, or another chosen type?
Short Answer
By the end of this page, you will understand how to type objects with dynamic keys in TypeScript so that every value must match a specific type. You will learn index signatures, the Record utility type, how assignment checking works, and how to avoid common mistakes when working with key-value maps.
Concept
In TypeScript, a plain empty object like this:
const stuff = {};
does not automatically mean “an object whose keys are strings and whose values are strings.” It simply means an object with no known properties yet.
If you want an object to behave like a dictionary or map—where keys are looked up dynamically—you should give it an explicit type.
The core feature for this is an index signature.
const stuff: { [key: string]: string } = {};
This tells TypeScript:
- keys are strings
- values must be strings
So now this is valid:
stuff["a"] = "foo";
stuff["b"] = "bar";
And this becomes a type error:
stuff["c"] = false;
This matters because many real programs use objects as lookup tables:
- configuration maps
- HTTP header collections
- error message dictionaries
- language translation tables
- cache objects
Without typing these objects, wrong values can slip in and cause bugs later. TypeScript helps catch those mistakes at development time.
Mental Model
Think of an indexed object as a set of labeled boxes.
- Each key is the label on a box
- Each value is what you put inside the box
If you declare the object as:
{ [key: string]: string }
then you are saying:
“You can create as many boxes as you want, with any string label, but every box must contain a string.”
So:
stuff["a"] = "hello"is okay because the box contains a stringstuff["c"] = falseis not okay because that box contains a boolean
The index signature is the rule for every box in the collection.
Syntax and Examples
Basic index signature
const stuff: { [key: string]: string } = {};
stuff["a"] = "foo";
stuff["b"] = "bar";
// stuff["c"] = false; // Error
This is the most direct way to say:
- any string key is allowed
- every value must be a string
Using Record
TypeScript also provides a built-in utility type:
const stuff: Record<string, string> = {};
stuff["a"] = "foo";
stuff["b"] = "bar";
// stuff["c"] = false; // Error
Record<string, string> means the same thing as:
{ [key: string]: string }
Many developers prefer Record because it is shorter and easier to read.
Example with another value type
You can replace string with any other type.
const scores: Record<string, number> = {};
scores["alice"] = 10;
scores["bob"] = 25;
// scores["carol"] = "high"; // Error
Example with predefined data
const messages: Record<string, string> = {
success: "Saved successfully",
error: "Something went wrong"
};
Every value in messages must be a string.
Step by Step Execution
Consider this example:
const stuff: Record<string, string> = {};
stuff["a"] = "foo";
stuff["b"] = "bar";
// stuff["c"] = false;
Step 1: Create the object
const stuff: Record<string, string> = {};
TypeScript reads this as:
stuffis an object- keys are strings
- values are strings
Step 2: Assign a string value
stuff["a"] = "foo";
- key:
"a"→ valid because it is a string - value:
"foo"→ valid because it is a string
So the assignment is allowed.
Step 3: Assign another string value
stuff["b"] = "bar";
Again:
- key is a string
- value is a string
So this is also allowed.
Step 4: Try assigning a boolean
stuff["c"] = false;
Now TypeScript checks the value type:
- key is still fine
- value is
boolean - expected value type is
string
So TypeScript reports an error.
That is exactly how the type annotation protects your object from invalid assignments.
Real World Use Cases
Configuration values
const envLabels: Record<string, string> = {
dev: "Development",
prod: "Production"
};
Useful when every entry should be plain text.
Translation dictionaries
const en: Record<string, string> = {
welcome: "Welcome",
logout: "Log out"
};
Translation keys are dynamic, but values should stay strings.
API error message maps
const errorMessages: Record<string, string> = {
NOT_FOUND: "Resource not found",
UNAUTHORIZED: "Please sign in"
};
Header or metadata storage
const headers: Record<string, string> = {
"content-type": "application/json",
authorization: "Bearer token"
};
Lookup tables in scripts
const fileTypes: Record<string, string> = {
jpg: "image",
mp4: "video",
pdf: "document"
};
Real Codebase Usage
In real projects, indexed object types are often used for flexible data structures where the exact keys are not known in advance.
Common patterns
Validation output
const errors: Record<string, string> = {};
if (!email) {
errors.email = "Email is required";
}
A form can have many field names, but every error message should be a string.
Configuration maps
const featureFlags: Record<string, boolean> = {
darkMode: true,
betaSearch: false
};
Same idea, different value type.
Guarding missing values
const messages: Record<string, string> = {
hello: "Hi"
};
function getMessage(key: string): string {
return messages[key] ?? "Unknown message";
}
Even when the value type is string, a lookup may still be missing at runtime, so developers often use fallback values.
Combining with named properties
type UserLabels = {
title: string;
[key: string]: string;
};
This means:
titlemust be a string- any other string key must also have a string value
This is useful when you know some properties ahead of time but still want dynamic keys.
Common Mistakes
1. Leaving the object untyped
const stuff = {};
stuff["a"] = "foo";
This does not clearly describe the intended shape of the object. Always add a type when you want dictionary-like behavior.
Correct:
const stuff: Record<string, string> = {};
2. Confusing key type with value type
const stuff: Record<string, string> = {};
This means:
- keys are strings
- values are strings
It does not mean only certain string keys are allowed. Any string key is allowed.
3. Expecting lookup safety for missing keys
const stuff: Record<string, string> = {};
console.log(stuff["missing"]);
At runtime, this may be undefined if the key does not exist.
Type annotations control assignment rules, but they do not create missing values automatically.
A safer approach is:
const value = stuff["missing"] ?? "default";
4. Mixing incompatible named properties with an index signature
Broken example:
type BadExample = {
count: number;
[key: string]: string;
};
This fails because count is a number, but the index signature says all string-keyed properties must be string.
Fix it by making the types compatible:
type GoodExample = {
countLabel: string;
[key: string]: string;
};
5. Using Object or {} when you mean a dictionary
const stuff: Object = {};
or
const stuff: {} = {};
These are too vague for indexed value checking. Use Record<string, T> or an index signature instead.
Comparisons
| Approach | Example | Best for | Notes |
|---|---|---|---|
| Index signature | { [key: string]: string } | Direct object typing | Very explicit |
Record | Record<string, string> | Readable dictionary types | Common and concise |
| Fixed object type | { a: string; b: string } | Known property names | Does not allow arbitrary keys easily |
Map | Map<string, string> | Advanced key-value storage | Different API from plain objects |
Index signature vs fixed properties
type Fixed = {
a: string;
b: string;
};
Use this when keys are known ahead of time.
type Dynamic = {
[key: string]: string;
};
Use this when keys are dynamic.
Object vs Map
const obj: Record<string, string> = {};
obj["x"] = "value";
const map = new Map<string, string>();
map.set("x", "value");
Choose a plain object when:
- keys are strings
- JSON-like data is convenient
- simple property access is enough
Choose Map when:
- you need methods like
.set(),.get(),.has() - insertion order or frequent mutations matter
- keys may not be strings
Cheat Sheet
// Index signature
const data: { [key: string]: string } = {};
// Record utility type
const data2: Record<string, string> = {};
Rules
stringkey type means any string key is allowed- value type controls what can be assigned
- wrong value types cause compile-time errors
Record<string, T>is equivalent to{ [key: string]: T }
Examples
const names: Record<string, string> = {};
names.first = "Alice";
names["last"] = "Smith";
// names.age = 30; // Error
Common alternatives
Record<string, number>
Record<string, boolean>
Record<string, string[]>
Watch out
const x = {}is not enough for a typed dictionary- missing keys may still return
undefinedat runtime - named properties must match the index signature type
Good default choice
const stuff: Record<string, string> = {};
FAQ
How do I type an object with string keys and string values in TypeScript?
Use either of these:
const data: { [key: string]: string } = {};
or:
const data: Record<string, string> = {};
What is the difference between Record<string, string> and { [key: string]: string }?
They mean the same thing. Record is just a built-in utility type that is often shorter and cleaner.
Why does const stuff = {} not enforce string values?
Because {} by itself does not describe a dictionary of string values. TypeScript needs an explicit type annotation.
Can I use types other than string for the values?
Yes. For example:
const flags: Record<string, boolean> = {};
const scores: Record<string, number> = {};
Can I restrict the keys too?
Yes, if the keys are known ahead of time:
type Keys = "a" | "b" | "c";
const data: Record<Keys, string> = {
a: "x",
b: "y",
c: "z"
};
Should I use an object or a Map?
Use an object for simple string-keyed data. Use Map when you need methods like .get(), .set(), or non-string keys.
Mini Project
Description
Build a small label dictionary for an application. The object should store message keys such as save, cancel, and delete, and every value must be a string. This demonstrates how to safely use dynamic keys while keeping values type-checked.
Goal
Create a typed dictionary object in TypeScript that only accepts string values for every string key.
Requirements
- Create an object that allows dynamic string keys.
- Ensure every value assigned to the object must be a string.
- Add at least three valid key-value pairs.
- Show one invalid assignment that TypeScript should reject.
- Read a value from the object and print it.
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.