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

> Understand TypeScript Record vs Map differences for static vs dynamic data. Learn when to use Record for string keys and Map for flexible collections to optimize your projects.

- Repository: [Microsoft/TypeScript](https://github.com/microsoft/typescript)
- Tags: deep-dive
- Published: 2026-02-21

---

**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`](https://github.com/microsoft/TypeScript/blob/main/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`](https://github.com/microsoft/TypeScript/blob/main/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`](https://github.com/microsoft/TypeScript/blob/main/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:

```typescript
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`](https://github.com/microsoft/TypeScript/blob/main/src/services/utilities.ts) for typed JSON structures and [`src/services/navigationBar.ts`](https://github.com/microsoft/TypeScript/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/microsoft/TypeScript/blob/main/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`](https://github.com/microsoft/TypeScript/blob/main/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`:

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

```

Conversely, the `Map` interface is defined in [`src/lib/es2015.collection.d.ts`](https://github.com/microsoft/TypeScript/blob/main/src/lib/es2015.collection.d.ts) as a runtime class with explicit method signatures and internal slots for key-value pairs:

```typescript
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`](https://github.com/microsoft/TypeScript/blob/main/src/services/utilities.ts) uses `Record` for typed JSON structures, while [`src/services/navigationBar.ts`](https://github.com/microsoft/TypeScript/blob/main/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:

```typescript
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.