TypeScript Hashmaps
You're building a cache for API responses, and suddenly your lookups return undefined even though you're sure the key exists. Or you're iterating through user preferences and they're coming back in random order when you need them sequential. These are the kinds of headaches that come from choosing the wrong hashmap implementation in TypeScript.
Hashmaps store key-value pairs and give you fast lookups through hashing algorithms. But TypeScript offers two very different ways to build them: plain JavaScript objects and the Map class. Each has trade-offs that'll bite you if you pick wrong.
This guide shows you how to create hashmaps, check for keys, manage entries, choose between objects and Maps, handle nested data, avoid common mistakes, and optimize for performance. You'll learn when to reach for each approach and how to sidestep the pitfalls that trip up most developers.
Creating a Hashmap in TypeScript
TypeScript offers two primary approaches for implementing hashmaps: JavaScript objects and the built-in Map class. Each has distinct advantages depending on your specific requirements.
Using Objects
The simplest way to create a hashmap in TypeScript is with a JavaScript object using an index signature:
const objectHashmap: { [key: string]: any } = {};
objectHashmap['key1'] = 'value1';
objectHashmap['key2'] = 'value2';
console.log(objectHashmap); // Output: { key1: 'value1', key2: 'value2' }
This approach is familiar to JavaScript developers and works well when your keys are strings or symbols.
Using Maps
For more flexibility, you can create a hashmap with a Map:
const mapHashmap: Map<string, any> = new Map();
mapHashmap.set('key1', 'value1');
mapHashmap.set('key2', 'value2');
console.log(mapHashmap); // Output: Map { 'key1' => 'value1', 'key2' => 'value2' }
The Map class offers advantages like allowing any data type as keys (not just strings) and maintaining insertion order.
Iterating Over a TypeScript Hashmap
Accessing all key-value pairs in a hashmap is a common operation. TypeScript offers several methods to iterate through hashmaps, with different approaches for objects and Maps.
Using Objects
To loop through a hashmap with an object, use a for...in loop:
const objectHashmap: { [key: string]: any } = { key1: 'value1', key2: 'value2' };
for (const key in objectHashmap) {
if (objectHashmap.hasOwnProperty(key)) {
console.log(`Key: ${key}, Value: ${objectHashmap[key]}`);
}
}
Or use Object.keys():
Object.keys(objectHashmap).forEach(key => {
console.log(`Key: ${key}, Value: ${objectHashmap[key]}`);
});
For more modern code, consider using Object.entries() to get key-value pairs directly:
Object.entries(objectHashmap).forEach(([key, value]) => {
console.log(`Key: ${key}, Value: ${value}`);
});
Using Maps
The Map class provides built-in iteration methods. The most straightforward is using forEach:
const mapHashmap: Map<string, any> = new Map([['key1', 'value1'], ['key2', 'value2']]);
mapHashmap.forEach((value, key) => {
console.log(`Key: ${key}, Value: ${value}`);
});
You can also use a for loop with destructuring to iterate through a Map's entries:
for (const [key, value] of mapHashmap) {
console.log(`Key: ${key}, Value: ${value}`);
}
This approach is cleaner and more readable than object iteration, demonstrating one advantage of using the Map class for hashmaps in TypeScript.
Checking if a Key Exists in a TypeScript Hashmap
Using Objects
To see if a key exists in a hashmap with an object, use the in operator:
const objectHashmap: { [key: string]: any } = { key1: 'value1', key2: 'value2' };
console.log('key1' in objectHashmap); // Output: true
console.log('key3' in objectHashmap); // Output: false
You can also use the hasOwnProperty method or the newer Object.hasOwn() function:
console.log(objectHashmap.hasOwnProperty('key1')); // Output: true
console.log(Object.hasOwn(objectHashmap, 'key1')); // Output: true (ES2022)
Using Maps
To see if a key exists in a hashmap with a Map, use the has() method:
const mapHashmap: Map<string, any> = new Map([['key1', 'value1'], ['key2', 'value2']]);
console.log(mapHashmap.has('key1')); // Output: true
console.log(mapHashmap.has('key3')); // Output: false
This method provides a clean and direct way to check for key existence, which can be especially useful when implementing complex filtering logic as described in complex-filters-in-convex.
Adding, Updating, and Deleting Entries in a TypeScript Hashmap
Using Objects
To add, update, or remove entries in a hashmap with an object, use this syntax:
const objectHashmap: { [key: string]: any } = { key1: 'value1', key2: 'value2' };
objectHashmap['key3'] = 'value3'; // Add a new entry
objectHashmap['key1'] = 'new_value1'; // Update an existing entry
delete objectHashmap['key2']; // Delete an entry
console.log(objectHashmap); // Output: { key1: 'new_value1', key3: 'value3' }
When managing key value pair data, remember that object properties always convert keys to strings, which can lead to unexpected behavior with numeric keys.
Using Maps
To add, update, or remove entries in a hashmap with a Map, use these methods:
const mapHashmap: Map<string, any> = new Map([['key1', 'value1'], ['key2', 'value2']]);
mapHashmap.set('key3', 'value3'); // Add a new entry
mapHashmap.set('key1', 'new_value1'); // Update an existing entry
mapHashmap.delete('key2'); // Delete an entry
console.log(mapHashmap); // Output: Map { 'key1' => 'new_value1', 'key3' => 'value3' }
These methods create a more consistent API compared to object syntax, which is particularly useful when building complex applications as discussed in functional-relationships-helpers.
Choosing Between JavaScript Objects and Maps for Hashmaps
The choice between objects and Maps isn't just about preference. Each shines in different scenarios, and picking the wrong one can hurt your application's performance.
When to Use Map vs Object: Decision Table
| Scenario | Use This | Why |
|---|---|---|
| Keys are non-strings (numbers, objects, etc.) | Map | Objects coerce all keys to strings |
| Frequent additions/deletions | Map | 2x faster for modifications with thousands of entries |
| Large datasets (1000+ entries) | Map | Significantly better performance at scale |
| Need guaranteed insertion order | Map | Objects don't guarantee order for all key types |
| Small, fixed structure (<10 keys) | Object | Better performance and less memory overhead |
| JSON serialization required | Object | JSON.stringify() works directly |
| Integer array-like keys | Object | V8 optimizes integer-indexed properties |
| Prototype pollution is a concern | Map | No inheritance from Object.prototype |
Key Type Behavior
Objects convert all keys to strings, which can cause unexpected bugs:
const objectHashmap: { [key: string]: string } = {};
objectHashmap[1] = 'one';
objectHashmap['1'] = 'one as string';
console.log(objectHashmap[1]); // Output: 'one as string' (both keys are the same!)
console.log(Object.keys(objectHashmap)); // Output: ['1'] (only one key exists)
Maps preserve the original key types:
const mapHashmap = new Map<number | string, string>();
mapHashmap.set(1, 'one');
mapHashmap.set('1', 'one as string');
console.log(mapHashmap.get(1)); // Output: 'one'
console.log(mapHashmap.get('1')); // Output: 'one as string'
console.log(mapHashmap.size); // Output: 2 (two distinct keys)
Prototype Pollution Risk
With objects, you risk colliding with inherited properties:
const objectHashmap: { [key: string]: any } = {};
console.log('toString' in objectHashmap); // Output: true (inherited from Object.prototype)
console.log(objectHashmap['toString']); // Output: [Function: toString]
Maps don't have this issue since they don't inherit from Object.prototype:
const mapHashmap = new Map<string, any>();
console.log(mapHashmap.has('toString')); // Output: false (no prototype pollution)
The typescript dictionary implementation techniques can help you create type-safe hashmaps. For Convex applications specifically, the types-cookbook demonstrates how to use validators and types to ensure data consistency.
Handling Complex Data Structures in TypeScript Hashmaps
When working with complex data structures in TypeScript hashmaps, you'll need effective strategies for nested objects, arrays, and deep copying. Here are some practical approaches:
Nested Objects
TypeScript makes it easy to define and work with nested object structures in hashmaps:
interface UserPreferences {
theme: string;
notifications: boolean;
language: string;
}
interface User {
name: string;
preferences: UserPreferences;
}
const userMap: { [userId: string]: User } = {
'user1': {
name: 'Alice',
preferences: {
theme: 'dark',
notifications: true,
language: 'en'
}
}
};
// Access nested properties
console.log(userMap['user1'].preferences.theme); // Output: 'dark'
// Update nested properties
userMap['user1'].preferences.theme = 'light';
This approach works well with Record<K, T> for stronger type definitions.
Arrays
When storing arrays in hashmaps, you can leverage array methods for data manipulation:
const categoryMap: { [key: string]: string[] } = {
'fruits': ['apple', 'banana', 'orange'],
'vegetables': ['carrot', 'broccoli', 'spinach']
};
// Add an item
categoryMap['fruits'].push('grape');
// Filter items
const aFruits = categoryMap['fruits'].filter(fruit => fruit.startsWith('a'));
console.log(aFruits); // Output: ['apple']
// Map to create derived data
const fruitLengths = categoryMap['fruits'].map(fruit => fruit.length);
console.log(fruitLengths); // Output: [5, 6, 6, 5]
The functional-relationships-helpers article demonstrates similar patterns for working with related data in Convex applications.
Deep Copying
When working with complex nested structures, be careful about reference sharing. For deep copies, consider these approaches:
// Using JSON (simple but has limitations with certain data types)
const deepCopy = JSON.parse(JSON.stringify(originalObject));
// Using structured clone (for modern browsers/environments)
const deepCopy2 = structuredClone(originalObject);
// Using a library like lodash
// import _ from 'lodash';
// const deepCopy3 = _.cloneDeep(originalObject);
For simple cases, object spreading can create shallow copies:
const shallowCopy = { ...originalObject };
For TypeScript applications handling complex nested structures, you may also need to consider memory management strategies. The complex-filters-in-convex article demonstrates techniques for efficiently filtering and manipulating complex data.
Common Pitfalls & Mistakes with TypeScript Hashmaps
Even experienced developers hit these gotchas when working with hashmaps. Here's how to avoid them.
Not Handling Undefined Values
The Map.get() method returns undefined when a key doesn't exist, but it also returns undefined if that's the actual stored value. This can lead to bugs:
const cache = new Map<string, string | undefined>();
cache.set('valid', undefined); // Intentionally storing undefined
cache.set('exists', 'value');
// Problem: Can't tell if key is missing or value is undefined
console.log(cache.get('valid')); // Output: undefined
console.log(cache.get('missing')); // Output: undefined (same result!)
// Solution: Always use has() to check for key existence
if (cache.has('valid')) {
const value = cache.get('valid'); // Now you know the key exists
console.log('Key exists with value:', value);
}
For objects, you'll want to use Object.hasOwn() or the in operator before accessing properties:
const userSettings: { [key: string]: string } = { theme: 'dark' };
// Problematic
const fontSize = userSettings['fontSize']; // undefined, but no error
// Better
if (Object.hasOwn(userSettings, 'fontSize')) {
const fontSize = userSettings['fontSize'];
// TypeScript knows fontSize exists here
}
Object Keys as Map Keys
Using objects as Map keys works, but equality is based on reference, not value:
interface CacheKey {
userId: string;
endpoint: string;
}
const apiCache = new Map<CacheKey, any>();
const key1: CacheKey = { userId: '123', endpoint: '/api/users' };
const key2: CacheKey = { userId: '123', endpoint: '/api/users' };
apiCache.set(key1, { data: 'cached response' });
console.log(apiCache.has(key2)); // Output: false (different object references!)
console.log(apiCache.size); // Output: 1
// Both keys look identical but are different objects in memory
console.log(key1 === key2); // Output: false
If you need value-based equality, serialize the object to a string key:
const apiCache = new Map<string, any>();
function createKey(obj: CacheKey): string {
return `${obj.userId}:${obj.endpoint}`;
}
const key1: CacheKey = { userId: '123', endpoint: '/api/users' };
const key2: CacheKey = { userId: '123', endpoint: '/api/users' };
apiCache.set(createKey(key1), { data: 'cached response' });
console.log(apiCache.has(createKey(key2))); // Output: true (string comparison works!)
Wrong Iteration Methods
A common mistake is trying to iterate over a Map with a regular for loop:
const userMap = new Map([
['user1', 'Alice'],
['user2', 'Bob']
]);
// Wrong: This doesn't work with Maps
// for (let i = 0; i < userMap.size; i++) { ... }
// Correct: Use for...of
for (const [key, value] of userMap) {
console.log(`${key}: ${value}`);
}
// Or forEach
userMap.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
For objects, don't forget to check hasOwnProperty with for...in:
const userObject: { [key: string]: string } = { name: 'Alice', role: 'admin' };
// Without hasOwnProperty check, you might iterate over inherited properties
for (const key in userObject) {
if (userObject.hasOwnProperty(key)) {
console.log(`${key}: ${userObject[key]}`);
}
}
// Modern alternative: Object.entries() automatically filters inherited properties
Object.entries(userObject).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
Forgetting Type Annotations
When you omit type annotations, TypeScript defaults to loose types that can hide bugs:
// Weak typing - accepts anything
const weakMap = new Map();
weakMap.set('key1', 'string');
weakMap.set(123, { obj: 'value' }); // No error, but inconsistent types
weakMap.set(true, [1, 2, 3]); // Also fine, but probably a mistake
// Strong typing - catches errors early
const strongMap = new Map<string, User>();
// strongMap.set(123, { name: 'Alice' }); // Error: Argument of type 'number' not assignable to 'string'
// strongMap.set('user1', 'Alice'); // Error: Type 'string' not assignable to 'User'
Always specify your Map types explicitly: Map<KeyType, ValueType>.
Boosting Performance When Using Hashmaps in TypeScript
To maximize the efficiency of TypeScript hashmaps, follow these practical performance optimization strategies backed by real-world benchmarks.
Performance Benchmarks: Map vs Object
The performance difference between Maps and objects varies dramatically based on your use case. Here's what the numbers show:
For Large Datasets (1,000+ entries):
- Insertions: Maps are approximately 2x faster than objects
- Deletions: Maps are 2-4x faster (the
deleteoperator is notoriously slow on objects) - Iterations: Maps are 4-5x faster for iterating through key-value pairs
- Key checks: Maps provide consistent O(1) lookups, while objects can degrade to O(n)
For Small Datasets (<10 entries):
- Read operations: Objects are 2-3x faster due to V8's inline caching
- Memory usage: Objects use significantly less memory (Maps can use 5-30x more memory)
Integer keys:
- Objects outperform Maps because V8 optimizes integer-indexed properties into a separate internal array
Here's a practical example showing when to switch:
// Small, read-heavy config - use an object
const appConfig: { [key: string]: string } = {
apiUrl: 'https://api.example.com',
timeout: '5000',
retries: '3'
};
// Large, frequently modified cache - use a Map
const userCache = new Map<string, User>();
// Frequently adding/removing users
userCache.set('user123', { name: 'Alice', role: 'admin' });
userCache.delete('user456'); // Much faster than `delete objectCache['user456']`
Memory Considerations
Maps use more memory than objects, especially for small datasets:
// Object: ~50 bytes for 1 key-value pair
const smallObject: { [key: string]: string } = { key1: 'value1' };
// Map: ~1500 bytes for 1 key-value pair (30x more memory!)
const smallMap = new Map<string, string>([['key1', 'value1']]);
// As size grows to 4 keys, the gap shrinks to ~5x more memory
// At 1000+ keys, the memory overhead becomes negligible percentage-wise
This matters when you're creating thousands of small hashmaps. If you're building a parser that creates a hashmap for each AST node, objects will be more memory-efficient. But for a single large cache shared across your application, Map's memory overhead is negligible compared to its performance benefits.
Avoid Unnecessary Iterations
Minimize loops and iterations when working with large hashmaps:
// Inefficient - multiple iterations
const keys = Object.keys(objectHashmap);
const filteredKeys = keys.filter(key => key.startsWith('user'));
const values = filteredKeys.map(key => objectHashmap[key]);
// More efficient - single iteration
const values = Object.entries(objectHashmap)
.filter(([key]) => key.startsWith('user'))
.map(([_, value]) => value);
Choose the Right Data Structure
Select the appropriate data structure based on your specific needs:
// Use a Set for unique values with fast lookups
const uniqueIds = new Set<string>();
uniqueIds.add('id1');
uniqueIds.add('id2');
console.log(uniqueIds.has('id1')); // Fast lookup
// Use WeakMap when keys are objects that need garbage collection
interface User {
id: string;
name: string;
}
interface UserData {
lastActive: Date;
preferences: object;
}
// WeakMap allows garbage collection of User objects when no other references exist
const userDataCache = new WeakMap<User, UserData>();
const user1: User = { id: '1', name: 'Alice' };
userDataCache.set(user1, { lastActive: new Date(), preferences: {} });
// When user1 is no longer referenced elsewhere, it can be garbage collected
// along with its associated data in the WeakMap
If you need to store unique values without duplicates, check out the TypeScript Set data structure. The usestate-less article demonstrates how choosing appropriate data structures can improve overall application performance.
Type Optimization
Use specific types instead of any to improve TypeScript's type checking and potential runtime performance:
// Less optimal
const anyMap: Map<string, any> = new Map();
// More optimal - specific types
const userMap: Map<string, User> = new Map();
Final Thoughts on TypeScript Hashmaps
Here's what matters when choosing your hashmap implementation:
Use Maps when:
- You have more than a few hundred entries
- You're frequently adding or deleting keys
- You need non-string keys or guaranteed insertion order
- You want to avoid prototype pollution
Use objects when:
- You have fewer than 10 fixed keys
- Your data is mostly read-only
- You need JSON serialization
- Memory usage is critical
Avoid common mistakes by:
- Always checking for key existence with
has()orObject.hasOwn() - Using explicit type annotations on your Maps
- Understanding that object keys are reference-based, not value-based
- Serializing object keys to strings when you need value-based equality
The performance difference can be dramatic at scale. A Map with thousands of entries will handle deletions 2-4x faster than an object, but an object with a handful of keys will beat a Map on reads. Choose based on your actual usage patterns, not assumptions.
These techniques will help you build faster, more reliable TypeScript applications whether you're implementing a cache, building a state manager, or just organizing data. Make the right choice upfront, and you'll save yourself debugging time later. Start developing TypeScript applications with confidence.