TypeScript Record vs Map: Key Differences and When to Use Each

Use Record for static, string-keyed objects with compile-time type safety and natural JSON serialization; choose Map for dynamic collections with non-string keys, frequent mutations, or insertion order requirements.

When architecting data structures in TypeScript, developers must choose between the compile-time utility of Record and the runtime flexibility of Map. Both constructs serve distinct purposes within the type system and JavaScript runtime, yet their differences significantly impact performance, serialization, and type safety. Understanding the TypeScript Record vs Map comparison is essential for writing maintainable, efficient code, as demonstrated by the TypeScript compiler's own implementation across its codebase.

Core Differences Between TypeScript Record and Map

Compile-Time Type vs Runtime Class

Record<K, T> is a utility type defined in src/lib/es5.d.ts at line 1595 that constructs an object type with keys constrained to K and values of type T. It generates no runtime code and compiles to a plain JavaScript object with standard property access.

Map<K, V> is a built-in ES2015 class defined in src/lib/es2015.collection.d.ts that creates a runtime map instance with dedicated internal slots and prototype methods including set, get, has, delete, and forEach.

Key Constraints and Flexibility

Record restricts keys to string-like types (string | number | symbol) and performs type checking at compile time. The resulting object owns these properties directly.

Map accepts any value as a key, including objects and functions, checking equality at runtime using the SameValueZero algorithm. This enables complex key relationships impossible with Record.

Performance and Serialization Characteristics

Object property access on Record instances is highly optimized by JavaScript engines (O(1)) with minimal overhead. Plain objects serialize naturally via JSON.stringify without transformation.

Map operations average O(1) but incur small runtime costs for internal data structure maintenance. Crucially, Map instances do not serialize directly to JSON; developers must convert them using Array.from() or similar methods before serialization.

When to Use Record in TypeScript

Choose Record when your data structure meets these criteria:

  • Static key definitions: The shape is known at compile time, such as configuration objects or enum-based lookup tables. The TypeScript compiler uses this pattern in src/services/navigationBar.ts for boolean flags indexed by enum keys.
  • String-constrained keys: All keys are strings, numbers, or symbols that exist as literal types or unions.
  • JSON serialization requirements: You need automatic serialization for API responses or local storage without conversion logic.
  • Zero runtime overhead: You want the fastest possible property access without wrapper objects or method call overhead.
  • Immutable patterns: You frequently use as const or readonly modifiers to create fixed dictionaries that never change after initialization.

When to Use Map in TypeScript

Select Map for scenarios requiring runtime flexibility:

  • Non-string keys: You need to use objects, functions, or complex values as keys. This is impossible with Record, which coerces keys to strings at runtime.
  • Dynamic key creation: Keys are generated at runtime based on user input or computed values rather than predefined literals.
  • Frequent mutations: You perform many additions and deletions, and you want to avoid creating new object references each time (common in React state management or caching layers).
  • Insertion order preservation: You rely on predictable iteration order matching insertion sequence, which Map guarantees per the ECMAScript specification.
  • Built-in collection methods: You prefer using size, clear(), forEach(), and other Map-specific APIs over manual object property management.

Practical Code Examples

Static Lookup with Record

The following example demonstrates a type-safe status message lookup using Record, where keys are constrained to a specific union type:

type Status = 'idle' | 'loading' | 'error';

const statusMessage: Record<Status, string> = {
  idle: 'Ready',
  loading: 'Fetching data…',
  error: 'Something went wrong',
};

function logStatus(s: Status) {
  console.log(statusMessage[s]);
}

logStatus('loading'); // → Fetching data…

This pattern appears frequently in the TypeScript compiler's own codebase, such as in src/services/utilities.ts for typed JSON structures and src/services/navigationBar.ts for enum-indexed flags.

Dynamic Keys with Map

When keys are determined at runtime or must be non-string values, Map provides the necessary flexibility:

interface User {
  id: number;
  name: string;
}

const userMap = new Map<number, User>();

userMap.set(1, { id: 1, name: 'Alice' });
userMap.set(2, { id: 2, name: 'Bob' });

function getUser(id: number): User | undefined {
  return userMap.get(id);
}

console.log(getUser(2)?.name); // → Bob

Unlike Record, this approach supports complex key types and preserves insertion order while providing O(1) access to the size property and built-in iteration methods.

Hybrid Approach Combining Both

For complex applications, you can combine both structures to leverage compile-time safety for static categories while maintaining runtime flexibility within each category:

type Category = 'fruits' | 'vegetables';
type Inventory = Record<Category, Map<string, number>>;

const inventory: Inventory = {
  fruits: new Map([['apple', 10], ['banana', 5]]),
  vegetables: new Map([['carrot', 7]]),
};

// Mutate the map without changing the record structure
inventory.fruits.set('orange', 3);

console.log(inventory.fruits.get('apple')); // 10

This pattern mirrors usage found in src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts, where the TypeScript team uses Record for static test configurations while employing Map-like structures for dynamic caching layers.

Implementation Details in the TypeScript Source

The distinction between these constructs is codified in the TypeScript library definitions. The Record utility type is defined in src/lib/es5.d.ts at line 1595 as a mapped type that constructs an object type with keys of type K and values of type T:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Conversely, the Map interface is defined in src/lib/es2015.collection.d.ts as a runtime class with explicit method signatures and internal slots for key-value pairs:

interface Map<K, V> {
    clear(): void;
    delete(key: K): boolean;
    forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
    get(key: K): V | undefined;
    has(key: K): boolean;
    set(key: K, value: V): this;
    readonly size: number;
}

The TypeScript compiler leverages Record extensively in its own implementation. For example, src/services/utilities.ts uses Record for typed JSON structures, while src/services/navigationBar.ts employs Record<boolean> for enum-indexed boolean flags. These real-world usages demonstrate the compiler team's preference for Record when modeling static, string-keyed dictionaries with known shapes at compile time.

Summary

  • Record<K, T> is a compile-time utility type that creates plain object types with string-like keys, offering zero runtime overhead and natural JSON serialization but limited to static key sets.
  • Map<K, V> is a runtime ES2015 class supporting any value type as keys, insertion order preservation, and built-in mutation methods, but requires conversion for JSON serialization.
  • Choose Record for configuration objects, lookup tables with known keys, API response types, and scenarios requiring simple serialization.
  • Choose Map for dynamic key generation, non-string keys, frequent additions/removals, or when you need collection methods like size and clear().
  • Both structures can coexist in hybrid patterns, such as Record<string, Map<string, number>>, to balance compile-time safety with runtime flexibility.

Frequently Asked Questions

Can I use objects as keys with TypeScript Record?

No, Record only supports string | number | symbol keys. When you attempt to use an object as a key with a Record, TypeScript will either raise a compile-time error or coerce the object to a string at runtime, resulting in [object Object]. For object keys, use Map, which accepts any value type including objects and functions, using SameValueZero equality comparison.

Is Map faster than Record for frequent additions and deletions?

While both offer O(1) average time complexity for access, Map is optimized for frequent mutations. Record (plain objects) can suffer from deoptimization in V8 and other engines when properties are frequently added or deleted, especially when the object transitions between "fast" and "dictionary" modes. Map maintains consistent performance characteristics for dynamic collections, making it preferable for caches or frequently updated registries.

How do I convert a Map to a Record in TypeScript?

To convert a Map to a Record, iterate over the map entries and build a new object. Note that this only works reliably if your Map uses string keys:

const map = new Map([['a', 1], ['b', 2]]);
const record: Record<string, number> = Object.fromEntries(map);
// Result: { a: 1, b: 2 }

For numeric or symbol keys, you must explicitly cast or map keys to strings first, as Record type parameters require keyof any but the runtime object will coerce all keys to strings.

Should I use Record or Map for JSON API responses?

Use Record for JSON API responses. Since Map instances do not serialize directly with JSON.stringify (they serialize to {}), they require manual conversion before network transmission. Record produces plain JavaScript objects that serialize naturally to JSON, making them ideal for API contracts, configuration objects, and data transfer objects (DTOs) that travel between client and server.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →