TypeScript Dictionary: Complete Guide to Type-Safe Objects

When working with key-value pairs in TypeScript, you need a reliable way to maintain type safety while accessing and manipulating data. Unlike languages with built-in dictionary types, TypeScript requires specific approaches to create type-safe dictionaries. This guide explores all implementation options, with practical examples to help you choose the right approach for your projects.
Key Takeaways
- TypeScript offers multiple ways to implement dictionaries: index signatures,
Record
utility, andMap
- Choose implementation based on your needs: index signatures for simple dictionaries,
Record
for specific key sets, andMap
for advanced features - Leverage TypeScript’s type system to ensure compile-time safety for dictionary operations
- Consider performance implications when selecting between object-based dictionaries and Map
- Use advanced type techniques like mapped types for complex dictionary requirements
What is a TypeScript Dictionary?
A dictionary in TypeScript is a data structure that stores key-value pairs, allowing efficient lookups by key. While TypeScript doesn’t have a native Dictionary
type, it provides several ways to implement dictionary-like structures with full type safety.
A type-safe dictionary ensures:
- Keys and values conform to specific types
- Compile-time errors for type mismatches
- Autocompletion and IntelliSense support
- Protection against runtime errors
Dictionary Implementation Options in TypeScript
Using Index Signatures
The most straightforward approach uses JavaScript objects with TypeScript index signatures:
// Basic index signature dictionary
const userScores: { [username: string]: number } = {};
// Adding entries
userScores[""alice""] = 95;
userScores[""bob""] = 87;
// Accessing values
console.log(userScores[""alice""]); // 95
// Type safety in action
userScores[""charlie""] = ""high""; // Error: Type 'string' is not assignable to type 'number'
This approach is simple but becomes cumbersome when reused across your codebase.
Creating a Reusable Dictionary Type
For better reusability, define a generic dictionary type:
// Generic dictionary type
type Dictionary<K extends string | number | symbol, V> = {
[key in K]: V;
};
// Usage with string keys and number values
const scores: Dictionary<string, number> = {};
scores[""math""] = 95;
scores[""science""] = 87;
// Usage with numeric keys
const idMapping: Dictionary<number, string> = {};
idMapping[1] = ""user_a"";
idMapping[2] = ""user_b"";
Using the Record Utility Type
TypeScript’s built-in Record
utility type provides a cleaner syntax:
// Using Record for fixed set of keys
type UserFields = ""name"" | ""email"" | ""role"";
const user: Record<UserFields, string> = {
name: ""Alice Smith"",
email: ""alice@example.com"",
role: ""Admin""
};
// Using Record with any string keys
const config: Record<string, any> = {};
config[""apiUrl""] = ""https://api.example.com"";
config[""timeout""] = 5000;
The Record
type is especially useful when you need to enforce a specific set of keys using string literal types.
Using JavaScript’s Map with TypeScript
For more advanced features, use JavaScript’s Map
object with TypeScript generics:
// Type-safe Map
const userProfiles = new Map<string, {age: number, active: boolean}>();
// Adding entries
userProfiles.set(""alice"", {age: 28, active: true});
userProfiles.set(""bob"", {age: 34, active: false});
// Type checking works
userProfiles.set(""charlie"", {age: ""thirty""}); // Error: Type 'string' not assignable to type 'number'
// Accessing values
const aliceProfile = userProfiles.get(""alice"");
console.log(aliceProfile?.age); // 28
Comparing Dictionary Implementations
Feature Object with Index Signature Record Type Map Key types String, number, symbol Any type Any type Performance Fast for small datasets Fast for small datasets Better for frequent additions/deletions Memory usage Lower Lower Higher Order preservation No No Yes (insertion order) Iteration Requires Object.entries()
Requires Object.entries()
Built-in iterators Special methods No No has()
, delete()
, clear()
Prototype pollution risk Yes Yes No
Common Dictionary Operations
Checking if a Key Exists
// Using index signature
const hasKey = (dict: Record<string, unknown>, key: string): boolean => {
return key in dict;
};
// Using Map
const userMap = new Map<string, number>();
userMap.has(""alice""); // true/false
Adding and Updating Values
// Using index signature
type UserDict = Record<string, {name: string, role: string}>;
const users: UserDict = {};
// Adding
users[""u1""] = {name: ""Alice"", role: ""Admin""};
// Updating
users[""u1""] = {...users[""u1""], role: ""User""};
// Using Map
const userMap = new Map<string, {name: string, role: string}>();
userMap.set(""u1"", {name: ""Alice"", role: ""Admin""});
userMap.set(""u1"", {name: ""Alice"", role: ""User""}); // Updates existing entry
Deleting Keys
// Using index signature
delete users[""u1""];
// Using Map
userMap.delete(""u1"");
Iterating Through Dictionaries
// Object-based dictionary iteration
const scores: Record<string, number> = {math: 95, science: 87};
// Method 1: Object.entries()
for (const [subject, score] of Object.entries(scores)) {
console.log(`${subject}: ${score}`);
}
// Method 2: Object.keys()
Object.keys(scores).forEach(subject => {
console.log(`${subject}: ${scores[subject]}`);
});
// Map iteration
const scoreMap = new Map<string, number>([
[""math"", 95],
[""science"", 87]
]);
// Direct iteration
for (const [subject, score] of scoreMap) {
console.log(`${subject}: ${score}`);
}
// Using forEach
scoreMap.forEach((score, subject) => {
console.log(`${subject}: ${score}`);
});
Advanced Type Safety Techniques
Constraining Key Types
// Using string literal union types
type ValidKeys = ""id"" | ""name"" | ""email"";
const userData: Record<ValidKeys, string> = {
id: ""123"",
name: ""Alice"",
email: ""alice@example.com""
};
// Attempting to use invalid keys
userData.phone = ""555-1234""; // Error: Property 'phone' does not exist
Readonly Dictionaries
// Immutable dictionary
const constants: Readonly<Record<string, number>> = {
MAX_USERS: 100,
TIMEOUT_MS: 5000
};
constants.MAX_USERS = 200; // Error: Cannot assign to 'MAX_USERS' because it is a read-only property
Partial Dictionaries
// Dictionary with optional values
type UserProfile = {
name: string;
email: string;
age: number;
};
// Create a dictionary where each value can have some or all UserProfile properties
const profiles: Record<string, Partial<UserProfile>> = {};
profiles[""alice""] = { name: ""Alice"" }; // Valid even without email and age
profiles[""bob""] = { name: ""Bob"", email: ""bob@example.com"" }; // Valid
Using Mapped Types for Advanced Dictionaries
type SensorData = {
temperature: number;
humidity: number;
pressure: string;
metadata: object;
};
// Create a dictionary type that only includes number properties from SensorData
type NumericSensorDict = {
[K in keyof SensorData as SensorData[K] extends number ? K : never]: boolean;
};
// The resulting type only has temperature and humidity as valid keys
const sensorStatus: NumericSensorDict = {
temperature: true,
humidity: false
};
Performance Considerations
When choosing a dictionary implementation, consider:
- Data size: For small datasets with string keys, object-based dictionaries are efficient
- Frequent modifications:
Map
performs better for frequent additions and deletions - Memory usage: Object-based dictionaries use less memory than
Map
- Key types: If you need non-string keys, use
Map
Benchmark results for 10,000 operations:
Object access: ~25ms
Map access: ~45ms
Object insertion: ~30ms
Map insertion: ~50ms
Object deletion: ~20ms
Map deletion: ~15ms
Real-World Use Cases
Configuration Management
type AppConfig = Record<string, string | number | boolean>;
const config: AppConfig = {
apiUrl: ""https://api.example.com"",
timeout: 5000,
enableCache: true
};
Cache Implementation
class Cache<T> {
private store = new Map<string, {value: T, expiry: number}>();
set(key: string, value: T, ttlSeconds: number): void {
const expiry = Date.now() + (ttlSeconds * 1000);
this.store.set(key, {value, expiry});
}
get(key: string): T | null {
const item = this.store.get(key);
if (!item) return null;
if (item.expiry < Date.now()) {
this.store.delete(key);
return null;
}
return item.value;
}
}
const userCache = new Cache<{name: string, role: string}>();
userCache.set(""user:123"", {name: ""Alice"", role: ""Admin""}, 300);
State Management in React
interface UserState {
users: Record<string, {name: string, role: string}>;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
users: {},
loading: false,
error: null
};
// In a reducer
function userReducer(state = initialState, action: any): UserState {
switch (action.type) {
case 'ADD_USER':
return {
...state,
users: {
...state.users,
[action.payload.id]: {
name: action.payload.name,
role: action.payload.role
}
}
};
// Other cases...
default:
return state;
}
}
Error Handling Strategies
Handling Missing Keys
function safeGet<K extends string, V>(
dict: Record<K, V>,
key: string,
defaultValue: V
): V {
return (key in dict) ? dict[key as K] : defaultValue;
}
const users: Record<string, string> = {
""1"": ""Alice""
};
const userName = safeGet(users, ""2"", ""Unknown User"");
console.log(userName); // ""Unknown User""
Type Guards for Dictionary Values
function isUserDict(obj: unknown): obj is Record<string, {name: string, age: number}> {
if (typeof obj !== 'object' || obj === null) return false;
for (const key in obj as object) {
const value = (obj as any)[key];
if (typeof value !== 'object' ||
typeof value.name !== 'string' ||
typeof value.age !== 'number') {
return false;
}
}
return true;
}
// Usage
function processUserData(data: unknown) {
if (isUserDict(data)) {
// TypeScript knows data is Record<string, {name: string, age: number}>
for (const [id, user] of Object.entries(data)) {
console.log(`User ${id}: ${user.name}, ${user.age}`);
}
}
}
Conclusion
TypeScript dictionaries provide powerful ways to manage key-value data with full type safety. By choosing the right implementation approach based on your specific requirements, you can build more robust applications with fewer runtime errors. Whether you need simple string-keyed objects or complex dictionaries with advanced features, TypeScript’s type system ensures your dictionary operations remain type-safe throughout your codebase.
FAQs
Use Map when you need non-string keys, guaranteed key order preservation, frequent additions and deletions, or protection from prototype pollution. Maps provide built-in methods like has(), delete(), and clear() that make certain operations more convenient
Use string literal types with the Record utility type:nn`typescriptntype AllowedKeys = 'id' | 'name' | 'email';nconst user: Record<AllowedKeys, string> = {n id: '123',n name: 'Alice',n email: 'alice@example.com'n};n`nnThis ensures TypeScript will error if you try to use keys not in the AllowedKeys union
Not with object-based dictionaries, but you can with Map:nn`typescriptnconst userMap = new Map<{id: number}, string>();nconst key1 = {id: 1};nconst key2 = {id: 1}; // Different object referencennuserMap.set(key1, 'Alice');nconsole.log(userMap.get(key1)); // 'Alice'nconsole.log(userMap.get(key2)); // undefined (different object reference)n`nnNote that Map uses reference equality for object keys
Convert object to Map:nn`typescriptnconst obj: Record<string, number> = {a: 1, b: 2};nconst map = new Map(Object.entries(obj));n`nnConvert Map to object:nn`typescriptnconst mapInstance = new Map<string, number>([['a', 1], ['b', 2]]);nconst objFromMap = Object.fromEntries(mapInstance);n`
Use optional chaining and nullish coalescing:nn`typescriptnconst users: Record<string, {name: string} | undefined> = {n '1': {name: 'Alice'}n};nn// Safe access with fallbacknconst userName = users['2']?.name ?? 'Unknown';nconsole.log(userName); // 'Unknown'n`nnThis prevents runtime errors when accessing properties of potentially undefined values