Back

TypeScript Dictionary: Complete Guide to Type-Safe Objects

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, and Map
  • Choose implementation based on your needs: index signatures for simple dictionaries, Record for specific key sets, and Map 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:

  1. Data size: For small datasets with string keys, object-based dictionaries are efficient
  2. Frequent modifications: Map performs better for frequent additions and deletions
  3. Memory usage: Object-based dictionaries use less memory than Map
  4. 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

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers