Question

How to Dynamically Assign Properties to an Object in TypeScript

Question

In JavaScript, you can programmatically assign a property to an object like this:

const obj = {};
obj.prop = "value";

In TypeScript, this causes an error because the empty object is inferred as type {}:

const obj = {};
obj.prop = "value"; // Error: Property 'prop' does not exist on type '{}'

How can you assign new properties dynamically to an object in TypeScript while keeping the code type-safe?

Short Answer

By the end of this page, you will understand why TypeScript rejects adding unknown properties to {}, and how to solve it using index signatures, Record, optional properties, and type assertions. You will also learn which approach is safest in real projects and when each option makes sense.

Concept

TypeScript checks not only what values your code uses, but also what shape your objects are allowed to have.

When you write:

const obj = {};

TypeScript infers obj as {}, which means "an object with no known properties." After that, this is not allowed:

obj.prop = "value";

because prop is not part of the inferred type.

This is different from plain JavaScript, where objects are open and properties can be added freely at runtime.

Why this matters

TypeScript's restriction helps catch bugs such as:

  • misspelled property names
  • unexpected object shapes
  • assigning the wrong value types
  • using properties that were never defined

If you truly want dynamic keys, you need to tell TypeScript that the object is allowed to have them.

Common ways to allow dynamic properties

1. Index signature

Use this when property names are not known ahead of time.

const obj: { [key: string]: string } = {};
obj.prop = "value";
obj.another = "hello";

This means:

  • keys are strings
  • values must be strings

2. Record<K, V>

This is a built-in utility type that does the same job in a cleaner form.

const obj: Record<string, string> = {};
obj.prop = "value";

3. Explicit object type with optional properties

Use this when you know the property names in advance, but they may be added later.

type MyObject = {
  prop?: string;
};

const obj: MyObject = {};
obj.prop = "value";

4. Type assertion

This tells TypeScript to trust you.

const obj = {} as { [key: string]: string };
obj.prop = "value";

This works, but should be used carefully because assertions can hide type mistakes.

Best idea to remember

TypeScript does not block dynamic properties completely. It just requires you to describe them in the type.

Mental Model

Think of a TypeScript object type like a form with allowed fields.

  • {} means the form has no fields.
  • { prop?: string } means the form has one optional field called prop.
  • { [key: string]: string } means the form allows any field name, as long as each value is a string.

JavaScript says, "Add any field you want." TypeScript says, "Tell me what fields are allowed first."

That is the core idea: dynamic behavior is fine, but the type must describe it.

Take Quiz

Syntax and Examples

Basic problem

const obj = {};
obj.prop = "value"; // Error

obj is inferred as {}, so no properties can be added unless the type says so.

Solution 1: Index signature

const obj: { [key: string]: string } = {};
obj.prop = "value";
obj.city = "Paris";

This is useful when keys are dynamic.

Solution 2: Record

const settings: Record<string, string> = {};
settings.theme = "dark";
settings.language = "en";

Record<string, string> is equivalent to an index signature for string keys.

Solution 3: Optional known properties

type User = {
  name?: string;
  email?: string;
};

const user: User = {};
user.name = "Ava";

Use this when the property names are known, but may not exist immediately.

Solution 4: Mixed known and dynamic keys

type ApiResponse = {
  status: string;
  [key: string]: string;
};

const response: ApiResponse = { status: "ok" };
response.message = "Success";

This allows extra string properties while still requiring status.

Example with dynamic property name

const key = "prop";
const obj: Record<string, string> = {};
obj[key] = "value";

This is common when property names come from user input, config, or loops.

Step by Step Execution

Consider this example:

const scores: Record<string, number> = {};
scores.alice = 10;
scores.bob = 15;

Step by step:

  1. const scores: Record<string, number> = {};

    • Creates an empty object.
    • Tells TypeScript that any string key is allowed.
    • Tells TypeScript that every value must be a number.
  2. scores.alice = 10;

    • alice is treated as a string key.
    • 10 is a number, so this is valid.
  3. scores.bob = 15;

    • bob is another string key.
    • 15 is also valid.
  4. If you try this:

scores.charlie = "high";

TypeScript gives an error because the object only accepts number values.

That is the benefit of typing a dynamic object: you still get flexibility for keys, but the values stay safe.

Real World Use Cases

Dynamic object properties are common in real programs.

Configuration maps

const config: Record<string, string> = {};
config.apiUrl = "https://example.com";
config.mode = "production";

Counting items

const counts: Record<string, number> = {};
counts.apple = 3;
counts.orange = 5;

Grouping data by key

const usersById: Record<string, string> = {};
usersById["u1"] = "Alice";
usersById["u2"] = "Bob";

API response containers

const errors: Record<string, string> = {};
errors.email = "Email is required";
errors.password = "Password is too short";

Feature flags

const flags: Record<string, boolean> = {};
flags.newDashboard = true;
flags.betaMode = false;

In each case, the keys may vary, but the value type is still predictable.

Take Quiz

Real Codebase Usage

In real codebases, developers usually avoid using plain {} for objects that will grow later. Instead, they choose a type based on how predictable the keys are.

Common patterns

Dictionary objects

const cache: Record<string, string> = {};

Used for lookups, caches, ID maps, and grouped values.

Validation error maps

type ErrorMap = Record<string, string>;

const errors: ErrorMap = {};

Useful for forms where field names map to error messages.

Guarding unknown values

function setValue(obj: Record<string, string>, key: string, value: string) {
  obj[key] = value;
}

This makes a helper function reusable and type-safe.

Known structure with optional fields

type DraftUser = {
  name?: string;
  email?: string;
};

This is often better than a dynamic map when the set of fields is already known.

Using Map instead of plain object

In some codebases, developers use Map when keys are heavily dynamic or not just strings:

const data = new Map<string, number>();
data.set("a", 1);

This is useful when insertion order, frequent updates, or non-object dictionary behavior matters.

Practical advice

  • Use Record<string, T> for dynamic string-keyed objects.
  • Use explicit interfaces or types for known properties.
  • Use type assertions sparingly.
  • Prefer the most specific type you can describe.

Common Mistakes

1. Using {} and expecting it to behave like JavaScript

Broken code:

const obj = {};
obj.name = "Sam";

Why it fails:

  • {} does not mean "any object with any properties"
  • it means "object with no known properties"

Fix:

const obj: Record<string, string> = {};
obj.name = "Sam";

2. Using any too quickly

Broken code:

const obj: any = {};
obj.name = 123;
obj.active = false;

Why it is a problem:

  • this removes type safety
  • TypeScript can no longer help you catch mistakes

Better:

const obj: Record<string, string | boolean | number> = {};

Only use any when absolutely necessary.

3. Using an index signature with the wrong value type

Broken code:

const obj: Record<string, string> = {};
obj.count = 10;

Fix:

const obj: Record<string, number> = {};
obj.count = 10;

Or if values can vary:

const obj: Record<string, string | number> = {};

4. Using optional properties when keys are truly dynamic

Broken approach:

type Data = {
  a?: string;
  b?: string;
};

const data: Data = {};
data.c = "hello"; // Error

Fix:

const data: Record<string, string> = {};
data.c = "hello";

5. Forgetting bracket notation for variable keys

Broken code:

const key = "name";
const obj: Record<string, string> = {};
obj.key = "Ava";

This creates a property literally named key.

Correct:

obj[key] = "Ava";

Use dot notation for fixed names and bracket notation for variable names.

Comparisons

ApproachBest forExampleNotes
{}Empty object with no added propertiesconst obj = {};Too restrictive for dynamic assignment
Optional propertiesKnown fields added latertype T = { name?: string }Good when keys are known in advance
Index signatureUnknown string keys{ [key: string]: string }Flexible and explicit
Record<string, T>Dynamic dictionary objectsRecord<string, number>Cleaner version of an index signature
anyTemporary escape hatchconst obj: any = {}Removes safety; avoid if possible
Map<K, V>Dynamic key-value storage with map behaviornew Map<string, number>()Useful alternative to objects

Record vs optional properties

  • Use Record<string, T> when keys are not known ahead of time.
  • Use optional properties when the keys are known, but may be missing initially.

Object vs Map

  • Use an object when you want simple JSON-like data.
  • Use Map when you need dedicated key-value operations such as .set(), .get(), and .has().
Take Quiz

Cheat Sheet

Quick reference

Problem

const obj = {};
obj.prop = "value"; // Error

Allow any string key with string values

const obj: Record<string, string> = {};
obj.prop = "value";

Equivalent index signature

const obj: { [key: string]: string } = {};

Known property added later

type T = { prop?: string };
const obj: T = {};
obj.prop = "value";

Variable key

const key = "prop";
const obj: Record<string, string> = {};
obj[key] = "value";

Mixed value types

const obj: Record<string, string | number | boolean> = {};

Avoid when possible

const obj: any = {};

Rules to remember

  • {} does not mean "any shape"
  • TypeScript needs the object shape described up front
  • Use Record<string, T> for dynamic keys
  • Use optional properties for known fields
  • Use bracket notation for variable property names

FAQ

Why does TypeScript reject obj.prop = "value" on {}?

Because {} is inferred as an object with no known properties. TypeScript only allows properties that exist in the type.

What is the best way to add dynamic properties in TypeScript?

Usually Record<string, T> is the clearest choice when keys are dynamic and values share one type.

Should I use any for dynamic objects?

Usually no. any disables type checking. Prefer Record, index signatures, or a proper interface.

When should I use optional properties instead of Record?

Use optional properties when the property names are known ahead of time but may be assigned later.

Is Record<string, string> the same as { [key: string]: string }?

Yes, for this use case they represent the same idea: any string key with string values.

Can dynamic object properties have different value types?

Yes. Use a union type such as Record<string, string | number | boolean>.

Should I use an object or a Map for dynamic keys?

Use an object for simple structured data. Use Map when you need map-specific operations or want a dedicated key-value collection.

Take Quiz

Mini Project

Description

Build a simple score tracker where player names are used as dynamic object keys. This demonstrates how to safely add properties to an object in TypeScript when the keys are not known in advance.

Goal

Create a TypeScript object that stores player scores using dynamic property assignment with proper typing.

Requirements

Create an empty score object that allows dynamic string keys. Add at least three player scores programmatically. Print one individual score and then print the full object. Ensure all score values are numbers. Do not use any.

Take Quiz

Keep learning

Related questions

View more