TypeScript Map
In the modern JavaScript ecosystem, the Map object is a powerful tool for managing collections of key-value pairs. While the standard ECMAScript Map provides a robust foundation, TypeScript elevates this data structure by introducing Generics. This addition allows developers to enforce strict type constraints on both keys and values, catching potential logic errors during development rather than at runtime.
A Map is a collection of keyed data items, much like an Object. However, unlike traditional objects, a Map allows keys of any type—including functions, objects, and even primitive values like NaN. Furthermore, a Map maintains the insertion order of its elements, ensuring that when you iterate over it, the items appear in the exact order they were added.
In TypeScript, we utilize the Map<K, V> syntax, where:
Krepresents the type of the keys.Vrepresents the type of the values.
Initialization and Type Safety
When you declare a Map with specific generic types, TypeScript ensures that every subsequent operation adheres to those rules.
// Creating an empty Map: keys are strings, values are numbers
const userScores = new Map<string, number>();
// Initializing a Map with predefined entries
const inventory = new Map<number, string>([
[101, "Laptop"],
[102, "Smartphone"],
]);
// Type Safety in Action
userScores.set("Alice", 95); // Success
// userScores.set(123, 100); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
By defining these types upfront, you eliminate the guesswork involved in managing dynamic collections.
Core API and Essential Operations
The Map API is intuitive and provides several built-in methods for efficient data manipulation.
set(key, value): Adds or updates an element. It returns theMapinstance itself, which enables method chaining.get(key): Retrieves the value associated with a key. Crucially, in TypeScript, if the key does not exist, the return type isV | undefined.has(key): Returns a boolean indicating whether a key exists in the collection.delete(key): Removes a specific element by its key.clear(): Wipes the entire collection.size: A property that returns the current number of elements.
const sessionMap = new Map<string, boolean>();
// Using method chaining
sessionMap.set("user_1", true).set("user_2", false);
console.log(sessionMap.get("user_1")); // true
console.log(sessionMap.get("user_3")); // undefined (Note the potential for undefined!)
if (sessionMap.has("user_2")) {
console.log("User 2 exists in the session.");
}
console.log(sessionMap.size); // 2
sessionMap.delete("user_2");
console.log(sessionMap.size); // 1
Pro Tip: Because get() can return undefined, TypeScript's strict null checking will force you to handle the "missing key" scenario. This prevents the common "cannot read property of undefined" runtime errors.
Iterating Over a Map
Since Map implements the iterable protocol, there are several elegant ways to traverse its contents.
1. Using for...of (Entries)
The most common way to iterate is by destructuring the key and value directly.
const roles = new Map<string, string>([
["admin", "Full Access"],
["editor", "Limited Access"],
]);
for (const [role, description] of roles) {
console.log(`${role}: ${description}`);
}
2. Using forEach
The forEach method follows the signature (value, key, map). Note that the value comes before the key in the callback arguments.
roles.forEach((desc, role) => {
console.log(`${role} has ${desc}`);
});
3. Iterating Keys or Values Only
If you only need one side of the pair, use .keys() or .values().
for (const role of roles.keys()) {
console.log("Role name:", role);
}
Data Transformation: Map, Array, and Object
In real-world applications, you often need to convert data between different formats.
const dataMap = new Map<string, number>([["a", 1], ["b", 2]]);
// 1. Map to Array
const arrayFromMap = Array.from(dataMap); // [["a", 1], ["b", 2]]
// 2. Array to Map
const newMap = new Map(arrayFromMap);
// 3. Map to Object
const objFromMap = Object.fromEntries(dataMap); // { a: 1, b: 2 }
// 4. Object to Map
const originalObj = { x: 10, y: 20 };
const mapFromObj = new Map(Object.entries(originalObj));
Choosing Between Map and Object
A frequent question is: "When should I use a Map instead of a plain object?" Here is a quick comparison to guide your decision:
| Feature | Object |
Map |
|---|---|---|
| Key Types | String, Symbol, or Number | Any type (Objects, Functions, etc.) |
| Ordering | Not strictly guaranteed for all keys | Guaranteed insertion order |
| Size | Manual (Object.keys(obj).length) |
Built-in .size property |
| Performance | Better for small, static structures | Better for frequent additions/removals |
| Serialization | Native JSON support | Requires manual conversion for JSON |
Recommendation: Use an Object (or Record<K, V>) when you have a fixed set of keys or need to serialize the data to JSON easily. Use a Map when you require dynamic keys, frequent updates, or need to maintain a specific order.
Practical Implementation: A Type-Safe TTL Cache
To see the true power of Map with TypeScript generics, let's implement a simple Time-To-Live (TTL) Cache. This class stores values that automatically "expire" after a certain duration.
class TTLCache<K, V> {
private cache = new Map<K, { value: V; expiry: number }>();
set(key: K, value: V, ttlMs: number): void {
const expiry = Date.now() + ttlMs;
this.cache.set(key, { value, expiry });
}
get(key: K): V | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiry) {
this.cache.delete(key); // Clean up expired entry
return undefined;
}
return entry.value;
}
}
// Usage
const tokenCache = new TTLCache<string, string>();
tokenCache.set("session_id", "xyz123", 5000); // Expires in 5 seconds
console.log(tokenCache.get("session_id")); // "xyz123"
By using generics <K, V>, this cache is completely reusable for any data type while remaining 100% type-safe.
A Crucial Distinction: Map vs. Mapped Types
It is easy to confuse the Map data structure with Mapped Types in TypeScript, but they serve entirely different purposes:
Map<K, V>is a runtime data structure used to store and manage data while your application is running.- Mapped Types are a compile-time type system feature used to create new types based on existing ones (e.g., making all properties of an interface
readonly).
Always remember: Map is about your data, while Mapped Types are about your type definitions.
Summary
The TypeScript Map<K, V> is an indispensable tool for developers handling complex, dynamic collections. By leveraging generics, it provides a layer of safety that prevents common bugs associated with missing keys or incorrect types. Whether you are building a high-performance cache or simply managing a list of user settings, understanding when to choose a Map over an Object will lead to cleaner, more predictable, and more robust code.