Question

How to Enforce String Values in Indexed TypeScript Objects

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 string
  • stuff["c"] = false is not okay because that box contains a boolean

The index signature is the rule for every box in the collection.

Take Quiz

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:

  • stuff is 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"
};
Take Quiz

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:

  • title must 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

ApproachExampleBest forNotes
Index signature{ [key: string]: string }Direct object typingVery explicit
RecordRecord<string, string>Readable dictionary typesCommon and concise
Fixed object type{ a: string; b: string }Known property namesDoes not allow arbitrary keys easily
MapMap<string, string>Advanced key-value storageDifferent 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
Take Quiz

Cheat Sheet

// Index signature
const data: { [key: string]: string } = {};

// Record utility type
const data2: Record<string, string> = {};

Rules

  • string key 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 undefined at 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.

Take Quiz

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

Keep learning

Related questions

View more