TypeScript dictionaries help you enforce type safety at compile time by ensuring that your data structures' keys and values conform to explicitly defined types. It reduces the risk of runtime errors and improves code reliability and maintainability.
This guide shows you how to use them effectively to prevent bugs and improve your development workflow.
In brief:
- TypeScript dictionaries define types for keys and values, preventing type mismatches.
Record<K, V>
offers better type checking than index signatures when you know your key set.- Use dictionaries for form validation, configuration, caching, and API integrations.
- TypeScript improves developer experience with autocompletion, type warnings, and safer refactoring.
What Is a TypeScript Dictionary?
A TypeScript dictionary is a key-value structure where you define types for both keys and values. It works like a JavaScript object, but with strict typing to catch errors during development.
This example uses an index signature to define a dictionary of numbers:
1interface NumberDictionary {
2 [key: string]: number;
3}
4
5const scores: NumberDictionary = {
6 "Alice": 95,
7 "Bob": 87,
8 "Charlie": 92
9};
10
11console.log(scores["Alice"]); // 95
12// scores["David"] = "A"; // Error: Type 'string' is not assignable to type 'number'.
This example defines a NumberDictionary
interface using an index signature, which showcases how TypeScript interfaces can enforce type safety in dictionaries. TypeScript will prevent us from assigning the wrong type of value, such as trying to set a string value for "David."
The index signature [key: string]: number
is the foundation of TypeScript dictionaries. It tells the compiler that this object can have any number of properties, as long as the keys are strings and the values are numbers.
Use a TypeScript dictionary when your keys are dynamic or unknown ahead of time. Common use cases include:
- Caching values in memory
- Creating lookup tables
- Managing configuration settings
- Storing form data
- Building simple key-based storage layers
Why Use TypeScript Dictionaries for Type-Safe Objects?
TypeScript dictionaries provide key advantages over plain JavaScript objects. They help you write safer, more maintainable code, especially when working with dynamic data or collaborating on large codebases.
Enforced Type Safety
TypeScript dictionaries enforce strict types for both keys and values. This catches errors at compile time instead of runtime.
In other words, they help you avoid common mistakes like assigning the wrong value type or using invalid keys.
1interface UserScores {
2 [key: string]: number;
3}
4
5const scores: UserScores = {
6 Alice: 95,
7 Bob: 87
8};
9
10// scores["Eve"] = "high"; // Compile error: Type 'string' is not assignable to type 'number'
Optimized Developer Experience
TypeScript dictionaries improve your development workflow with IDE support. You get:
- Autocomplete for known keys
- Inline type hints
- Compile-time error highlighting
These features make your code easier to write and review.
Improved Code Reliability and Maintainability
Dictionaries enforce structure, which improves consistency. This is helpful in large projects and team environments. For example, one fintech company reduced production bugs by switching from index signatures to Record<SpecificKey, Value>
types. That change helped catch typos and incomplete mappings during development.
Safer Refactoring
When you refactor code, TypeScript checks dictionary keys and values across your project. It flags missing updates and broken references, reducing the risk of silent errors.
How to Declare and Use a TypeScript Dictionary
TypeScript dictionaries provide a powerful way to create type-safe key-value structures. Building on TypeScript basics, let's explore the different approaches to implementing and using dictionaries in TypeScript.
Using Index Signatures
The most basic way to create a dictionary in TypeScript is by using an index signature. This approach allows you to define a flexible structure where keys are of a specific type (usually string) and values are of another type.
1interface Dictionary {
2 [key: string]: number;
3}
4
5const userScores: Dictionary = {
6 "alice": 95,
7 "bob": 87,
8 "charlie": 92
9};
10
11console.log(userScores["alice"]); // 95
Index signatures enable flexibility when keys are unpredictable or user-generated. They're handy for scenarios like form field validation or dynamic configuration objects.
You can also use a number or symbol as a key type, though string is most common:
1interface NumericDictionary {
2 [key: number]: string;
3}
4
5const numberToWord: NumericDictionary = {
6 1: "one",
7 2: "two",
8 3: "three"
9};
When working with index signatures, TypeScript enforces the value type when adding or updating entries:
1userScores["david"] = 88; // OK
2userScores["eve"] = "excellent"; // Error: Type 'string' is not assignable to type 'number'
Using Record\<K, V>
The Record
utility type provides a cleaner alternative to index signatures, especially when key sets are known. It's defined as Record<K, V>
, where K is the key type and V is the value type.
1type UserRole = "admin" | "user" | "guest";
2type RolePermissions = Record<UserRole, string[]>;
3
4const permissions: RolePermissions = {
5 admin: ["read", "write", "delete"],
6 user: ["read", "write"],
7 guest: ["read"]
8};
Record
provides better type safety and clearer code, particularly when the set of potential keys is finite. It's actually a mapped type defined internally in TypeScript:
1type Record<K extends keyof any, T> = {
2 [P in K]: T;
3};
This definition ensures that all keys of type K are present and map to values of type T.
Typing Keys and Values
When creating dictionaries, you can use various strategies to type keys and values effectively:
- String Literal Unions for Keys:
1type AllowedKeys = "config" | "data" | "metadata";
2type ConfigDictionary = Record<AllowedKeys, any>;
3
4const appConfig: ConfigDictionary = {
5 config: { version: "1.0" },
6 data: [1, 2, 3],
7 metadata: { lastUpdated: new Date() }
8};
- Enums for Keys:
1enum HttpMethod {
2 GET = "GET",
3 POST = "POST",
4 PUT = "PUT",
5 DELETE = "DELETE"
6}
7
8type ApiHandlers = Record<HttpMethod, (data: any) => void>;
9
10const handlers: ApiHandlers = {
11 [HttpMethod.GET]: (data) => console.log("GET", data),
12 [HttpMethod.POST]: (data) => console.log("POST", data),
13 [HttpMethod.PUT]: (data) => console.log("PUT", data),
14 [HttpMethod.DELETE]: (data) => console.log("DELETE", data)
15};
- Complex Value Types:
1interface UserData {
2 name: string;
3 age: number;
4 isActive: boolean;
5}
6
7type UserDictionary = Record<string, UserData>;
8
9const users: UserDictionary = {
10 "user1": { name: "Alice", age: 30, isActive: true },
11 "user2": { name: "Bob", age: 25, isActive: false }
12};
You can also use utility types like Partial<T>
for more flexible value structures:
1type PartialUserDictionary = Record<string, Partial<UserData>>;
2
3const partialUsers: PartialUserDictionary = {
4 "user1": { name: "Charlie" }, // Age and isActive are optional
5 "user2": { age: 35, isActive: true } // Name is optional
6};
Populating Dictionaries
There are several ways to create and populate TypeScript dictionaries:
- Empty Initialization:
1const emptyDict: Record<string, number> = {};
- Initializing with Predefined Values:
1const initialScores: Record<string, number> = {
2 "alice": 100,
3 "bob": 85
4};
- Converting from Arrays:
1const users = ["alice", "bob", "charlie"];
2const initialScores = Object.fromEntries(users.map(user => [user, 0])) as Record<string, number>;
When adding new entries or updating existing ones, TypeScript ensures type safety:
1initialScores["david"] = 90; // OK
2initialScores["eve"] = "high"; // Error: Type 'string' is not assignable to type 'number'
Accessing and Updating Dictionary Entries
Safely accessing dictionary values often involves handling potential undefined values:
1const score = initialScores["alice"]; // TypeScript knows this is a number
2const unknownScore = initialScores["unknown"]; // TypeScript infers this as number | undefined
You can use optional chaining or nullish coalescing for safer access:
1const safeScore = initialScores["unknown"] ?? 0; // Default to 0 if not found
To check for key existence, use the in
operator:
1if ("alice" in initialScores) {
2 console.log("Alice's score exists");
3}
Iterating over dictionaries can be done using for...in
loops or Object
methods:
1// Using for...in
2for (const user in initialScores) {
3 console.log(`${user}: ${initialScores[user]}`);
4}
5
6// Using Object.entries()
7Object.entries(initialScores).forEach(([user, score]) => {
8 console.log(`${user}: ${score}`);
9});
For transforming dictionary data, you can use Object.entries()
and Object.fromEntries()
:
1const doubledScores = Object.fromEntries(
2 Object.entries(initialScores).map(([user, score]) => [user, score * 2])
3) as Record<string, number>;
This pattern can be useful in some cases when working with Strapi v5's content structures, particularly for iterating over and displaying component properties, though it is not strictly required for transforming or validating data before saving.
Best Practices for Type-Safe TypeScript Dictionaries
Following best practices improves type safety, code maintainability, and your overall developer experience when working with TypeScript dictionaries. These recommendations help you avoid common mistakes and write more robust code.
1. Use Record\<K, V> for Known Key Sets
When you have a known set of keys, prefer using the Record
utility type over index signatures. This ensures full key coverage and prevents invalid keys.
1type Role = 'admin' | 'editor' | 'viewer';
2type Permissions = Record<Role, string[]>;
3
4const rolePermissions: Permissions = {
5 admin: ['create', 'read', 'update', 'delete'],
6 editor: ['read', 'update'],
7 viewer: ['read']
8};
Restricting keys catches typos and invalid values at compile time..
2. Prefer Union Types or Enums for Keys
Use string literal unions or enums to restrict dictionary keys and improve autocompletion:
1enum HttpStatus {
2 OK = 200,
3 NotFound = 404,
4 ServerError = 500
5}
6
7type StatusMessages = Record<HttpStatus, string>;
8
9const messages: StatusMessages = {
10 [HttpStatus.OK]: "Success",
11 [HttpStatus.NotFound]: "Resource not found",
12 [HttpStatus.ServerError]: "Internal server error"
13};
Restricting keys catches typos and invalid values at compile time.
3. Create Specific Interfaces for Value Types
Define value types using interfaces instead of primitives to improve readability and reuse.
1interface UserInfo {
2 name: string;
3 email: string;
4 lastLogin: Date;
5}
6
7type UserDirectory = Record<string, UserInfo>;
8
9const users: UserDirectory = {
10 "alice": { name: "Alice", email: "alice@example.com", lastLogin: new Date() },
11 "bob": { name: "Bob", email: "bob@example.com", lastLogin: new Date() }
12};
This pattern helps you enforce consistent data structures across your app..
4. Apply Utility Types for Flexibility
Use TypeScript utility types like Readonly<T>
or Partial<T>
to add constraints or flexibility to your dictionaries:
1type Config = Readonly<Record<string, string | number | boolean>>;
2
3const appConfig: Config = {
4 apiUrl: "https://api.example.com",
5 timeout: 3000,
6 enableCache: true
7};
8
9// This would cause a compile-time error
10// appConfig.apiUrl = "https://newapi.example.com";
Using Readonly
here prevents accidental modifications to the configuration object.
5. Use Consistent Naming Conventions
Adopt clear and consistent naming conventions for your dictionary types and variables. This improves code readability and maintainability:
1type UserRoleMap = Record<string, string>;
2type ProductInventory = Record<string, number>;
3
4const userRoles: UserRoleMap = { /* ... */ };
5const productStock: ProductInventory = { /* ... */ };
Naming your types clearly helps others understand your intent without reading the full implementation.
6. Create Reusable Generic Dictionary Types
Define generic dictionary patterns once and reuse them throughout your codebase.
1type SafeDictionary<K extends string, V> = Record<K, V>;
2
3type UserRoles = SafeDictionary<'admin' | 'editor' | 'viewer', string[]>;
4type ApiRoutes = SafeDictionary<'users' | 'posts' | 'comments', string>;
5
6const roles: UserRoles = {
7 admin: ['all'],
8 editor: ['create', 'edit'],
9 viewer: ['read']
10};
11
12const routes: ApiRoutes = {
13 users: '/api/users',
14 posts: '/api/posts',
15 comments: '/api/comments'
16};
Reusable types reduce duplication and ensure consistent structure across your project.
These techniques improve type safety and enhance the developer experience by providing better autocompletion, refactoring support, and error detection in your IDE. This is particularly valuable when working with Strapi v5, where type-safe code can help maintain the integrity of your content models and prevent errors when interacting with your API.
TypeScript Dictionary vs. Map vs. Object: A Comparison
When managing key-value data in TypeScript, you can use three structures: dictionaries, Map
, and plain JavaScript objects. Each offers different strengths based on your use case.
- TypeScript Dictionaries (via
Record<K, V>
or index signatures) offer strong type safety and IDE support. Keys must bestring
orsymbol
. Use them for configuration, validation, or data that needs to be serialized to JSON, such as Strapi API responses. - Map supports any key type, preserves insertion order, and performs better for frequent additions and deletions. It includes built-in methods like
.set()
and.get()
but is not directly JSON-serializable. - Plain Objects work like dictionaries but lack static typing unless you manually define an index signature or interface. Use them in simple scripts or legacy codebases where type safety is less critical.
Choose TypeScript dictionaries when keys are strings and type safety matters. Use Map when key types are complex or order matters. Use plain objects when simplicity is more important than strict typing.
Real-World Use Cases for TypeScript Dictionaries
TypeScript dictionaries are ideal when you need safe, reliable key-value access in real-world applications.
- Form Validation: Store field-level validation rules with type safety across complex forms.
- Internationalization (i18n): Organize translation strings by language and key. Prevent missing values with typed nested dictionaries.
- Feature Flags: Track feature toggles with type-checked access to avoid undefined behavior during deployment.
- API Response Caching: Store and retrieve responses by URL or endpoint, using types to structure cached data consistently.
- Configuration Management: Use dictionaries to group settings across environments (dev, staging, prod) with confidence in key names and value types.
- Role-Based Permissions: Map user roles to actions for clean, maintainable access control.
- Strapi API Development: Use dictionaries in Strapi v5 for type-safe content models, plugin configs, and response mappings. Strong typing improves reliability when defining collections, components, or controllers.
- Enterprise Content Relationships: Manage relationships between content types in Strapi using dictionaries to keep entity maps consistent and type-safe.
When integrating with third-party APIs, dictionaries help you build flexible, type-safe models for processing responses. For more guidance, review our API integration overview.
Unlocking the Power of TypeScript Dictionaries
TypeScript dictionaries solve the challenge of creating type-safe key-value structures. Selecting between index signatures or the Record
utility type can prevent runtime errors and improve code maintainability.
Key benefits of using TypeScript include compile-time error detection, strong IDE support, and safer refactoring. These features make dictionaries more reliable and robust, mainly for building APIs, managing state, or creating dynamic content structures in Strapi v5.
Adopting these practices reduces bugs and enhances the developer experience. In environments like Strapi, where content management and modeling demand flexibility and type safety, mastering TypeScript dictionaries ensures immediate and long-term value.