How to Implement a Map<string, string> in TypeScript: Best Practices for Map TypeScript

The most idiomatic way to implement a string-to-string map in TypeScript is to use the native ES2015 Map<string, string> type, which provides O(1) lookups, insertion order preservation, and a type-safe API.

When working with key-value pairs in TypeScript, choosing the right data structure is critical for both performance and type safety. The Microsoft TypeScript repository provides comprehensive type definitions for the built-in JavaScript Map object, making it the standard choice for map typescript implementations. This article explores the idiomatic patterns, source code definitions, and best practices for implementing Map<string, string> based on the official TypeScript library files.

Map<string, string> vs. Record<string, string>

Before implementing a map, understand the distinction between Map and Record. The Map<string, string> type offers significant advantages over the Record<string, string> alternative defined in src/lib/es5.d.ts.

Map<string, string> provides explicit ordering guarantees, O(1) lookup complexity, and a rich API including keys(), values(), and entries() methods. It is the preferred choice when you need frequent insertions and deletions.

Record<string, string> represents a simple object type useful for JSON serialization scenarios where you need a plain object structure. However, it lacks iteration order guarantees and the full Map API.

Use Record only when interacting with APIs requiring plain objects; otherwise, prefer Map<string, string> for map typescript implementations.

Core Type Definitions in TypeScript

The official type definitions for Map reside in the TypeScript standard library files. Understanding these source files helps you leverage the full type system.

ES2015 Collection Definitions

The primary interface definition lives in src/lib/es2015.collection.d.ts. This file declares the core Map<K, V> interface and the ReadonlyMap<K, V> variant used across all TypeScript targets.

// Core interface from src/lib/es2015.collection.d.ts
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;
}

ESNext Extensions

For modern runtimes targeting ES2025+, src/lib/esnext.collection.d.ts adds convenience methods like getOrInsert and getOrInsertComputed:

// Extended interface from src/lib/esnext.collection.d.ts
interface Map<K, V> {
    getOrInsert(key: K, defaultValue: V): V;
    getOrInsertComputed(key: K, defaultValue: (key: K) => V): V;
}

Creating and Initializing Maps

Instantiate your map using explicit type parameters to prevent accidental insertion of non-string values.

Zero-argument constructor for empty maps:

const userPreferences: Map<string, string> = new Map();

Initializer array constructor for pre-populated data:

const config: Map<string, string> = new Map([
  ['apiUrl', 'https://api.example.com'],
  ['timeout', '5000']
]);

Always declare Map<string, string> explicitly rather than relying on type inference when initializing empty maps. This ensures type safety throughout your application.

Working with Map Methods

The Map API provides mutating methods that return the map instance for method chaining.

const translations = new Map<string, string>();

// Chaining set operations
translations
  .set('hello', 'bonjour')
  .set('goodbye', 'au revoir')
  .set('thank you', 'merci');

// Checking existence
const hasHello = translations.has('hello'); // true

// Retrieving values
const greeting = translations.get('hello'); // "bonjour"
const missing = translations.get('unknown'); // undefined

// Removing entries
translations.delete('goodbye');
translations.clear(); // Removes all entries

Note that set, delete, and clear mutate the map in-place. They return the map itself to enable fluent chaining patterns.

Iteration Patterns

Maps preserve insertion order during iteration. Prefer for...of loops or the forEach method for traversing entries.

Destructured iteration (recommended):

const settings = new Map<string, string>([
  ['theme', 'dark'],
  ['language', 'typescript']
]);

for (const [key, value] of settings) {
  console.log(`${key} → ${value}`);
}

forEach method:

settings.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

Both approaches maintain the original insertion order, unlike traditional object property enumeration.

Advanced TypeScript Map Patterns

Read-only Map Exposure

When exposing maps to consumers that should not mutate the data, return ReadonlyMap<string, string>:

function getSystemConfig(): ReadonlyMap<string, string> {
  const internalMap = new Map<string, string>();
  internalMap.set('version', '2.0.0');
  return internalMap;
}

const config = getSystemConfig();
config.set('newKey', 'value'); // Error: Property 'set' does not exist on type 'ReadonlyMap<string, string>'

ESNext Get-or-Insert Utilities

For cached computations or default value patterns, use the ESNext getOrInsert method available when targeting ES2025 or later:

const cache = new Map<string, string>();

function fetchData(id: string): string {
  return cache.getOrInsert(id, `computed-${id}`);
}

// Equivalent to:
// if (!cache.has(id)) cache.set(id, `computed-${id}`);
// return cache.get(id)!;

Performance and Serialization Considerations

Performance characteristics: For large datasets with frequent insertions and deletions, Map outperforms plain objects. The implementation uses hash table optimizations that maintain O(1) average time complexity for set and get operations.

JSON serialization: Map instances are not directly JSON-serializable. Convert to an array of entries before stringifying:

const data = new Map([['key', 'value']]);
const serialized = JSON.stringify(Array.from(data)); // [["key","value"]]
const revived = new Map(JSON.parse(serialized) as [string, string][]);

This conversion pattern ensures compatibility with APIs expecting plain objects or arrays while preserving your map typescript type safety internally.

Summary

  • Use Map<string, string> instead of Record<string, string> when you need ordering guarantees, O(1) lookups, or the full Map API.
  • Reference src/lib/es2015.collection.d.ts for core type definitions and src/lib/esnext.collection.d.ts for modern utility methods.
  • Always specify type parameters explicitly: const map: Map<string, string> = new Map().
  • Leverage method chaining with set(), delete(), and clear() which return the map instance.
  • Expose ReadonlyMap<string, string> to prevent external mutation of internal map state.
  • Convert to Array.from(map) before JSON.stringify() since Maps are not directly serializable.
  • Consider getOrInsert and getOrInsertComputed for caching patterns when targeting ES2025+.

Frequently Asked Questions

What is the difference between Map and Record in TypeScript?

Map is a built-in JavaScript class providing ordered key-value storage with methods like set(), get(), and has(), while Record is a TypeScript utility type representing a plain object with fixed keys. Use Map when you need to preserve insertion order or frequently add/remove keys; use Record for simple static structures that need JSON serialization.

How do I make a Map immutable in TypeScript?

Declare the type as ReadonlyMap<string, string> or cast an existing map using as ReadonlyMap<string, string>. This removes mutating methods like set(), delete(), and clear() from the type definition, preventing accidental modifications while still allowing reads and iteration.

Can I use Map with string keys in older JavaScript environments?

The Map constructor requires ES2015 (ES6) or later. TypeScript compiles Map usage to compatible code when targeting older environments, but you may need to include a polyfill for runtime support in browsers older than IE11. The type definitions in src/lib/es2015.collection.d.ts provide the necessary TypeScript support regardless of target.

Why does JSON.stringify return an empty object for my Map?

JavaScript's JSON.stringify does not enumerate Map entries because they are not stored as object properties. Convert the map to an array using Array.from(myMap) or [...myMap] before stringifying to preserve the key-value pairs as nested arrays.

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 →