Key-Value Pairs in TypeScript
You're pulling user data from an API and storing it in an object. Everything works fine until production, where you discover you can't reliably iterate over the keys without type errors, or worse—the object's grown so large that lookups are sluggish. Should you use a plain object? A Map? A Record type?
This guide covers the practical ways to work with key-value pairs in TypeScript—from choosing the right data structure to handling iteration type safety issues that trip up even experienced developers.
Defining a TypeScript Interface for Key-Value Pairs
You can define a TypeScript interface for a key-value pair using the following syntax:
interface KeyValuePair {
key: string;
value: any;
}
This interface creates a simple structure with a key property of type string and a value property that can be of any type.
For more type safety, you can use generics to specify the types of both keys and values:
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// Example usage
const stringNumberPair: KeyValuePair<string, number> = {
key: 'age',
value: 30
};
By using generics, you ensure that the types of both the key and value are consistent throughout your application, reducing potential runtime errors. This approach is particularly useful when working with Typescript objects that need consistent typing.
If you're working on a project that uses Convex, this approach to typing key-value pairs integrates well with Convex's type system.
Using TypeScript Maps for Key-Value Storage
Maps in TypeScript are built for dynamic key-value storage with frequent updates. Here's a practical example using a shopping cart:
class ShoppingCart {
private items = new Map<string, number>();
addItem(productId: string, quantity: number): void {
const currentQty = this.items.get(productId) ?? 0;
this.items.set(productId, currentQty + quantity);
}
removeItem(productId: string): void {
this.items.delete(productId);
}
getTotal(): number {
let total = 0;
this.items.forEach((qty) => {
total += qty;
});
return total;
}
getTotalItems(): number {
return this.items.size; // Built-in size property
}
}
const cart = new ShoppingCart();
cart.addItem('product-123', 2);
cart.addItem('product-456', 1);
console.log(cart.getTotalItems()); // Output: 2
So, why would you reach for a Map over a plain object? Maps shine when:
- Keys can be any type, not just strings and symbols (you can use objects as keys)
- Insertion order is guaranteed when iterating
- Built-in methods like
.size,.has(),.delete()make manipulation cleaner - Better performance for frequent additions and removals, especially with large datasets
You can use foreach to iterate through a Map:
cart.items.forEach((quantity, productId) => {
console.log(`Product ${productId}: ${quantity} items`);
});
When working with Convex, Maps can be especially useful for client-side caching of query results, as described in complex filters in Convex.