Editor’s note: This article was last updated by Shalitha Suranga on 20 February 2024 to include advanced type checking techniques like adding, removing, and checking for keys.
Developers use a variety of data structures in codebases, all based on programming requirements. Storing key-value data pairs is a general requirement for most software development projects. For example, you may need to store WebSocket connection objects using unique connection string keys. The dictionary structure, also known as the map, is a well-known data structure that lets you store key-value data pairs in almost all programming languages.
There are several approaches for implementing dictionaries in JavaScript. We can use these JavaScript-based dictionary creation techniques in TypeScript too, but we have to apply some TypeScript features to make them type-safe.
Writing type-safe objects is a crucial requirement for writing readable, maintainable, and clean TypeScript code, so follow this tutorial to learn how to create type-safe dictionaries in TypeScript.
In a programming language, a dictionary is a typical data structure that stores data in key-value pairs. JavaScript dictionary creation strategies offer a way to create a dictionary structure that accepts dynamic data types.
For example, a simple dictionary you’ve created in JavaScript may accept even objects as values even though you might use it for storing numbers. Using TypeScript type-checking features, you can turn these dynamic JavaScript dictionaries into type-safe dictionaries that accept only a pre-defined key-value type declaration, i.e., string for keys, and number for values.
A type-safe dictionary triggers compile-time and linter errors if a developer tries to store data types that are not defined in accepted dictionary key-value types.
As mentioned earlier, JavaScript has several ways to create dictionaries. Let’s learn them first before making them type-safe with TypeScript.
Object
types in JavaScriptWe can use a generic JavaScript object as a dictionary structure. Using Object
-based dictionaries, you can store any value, but keys are limited to strings, numbers, and symbols. There are two primary ways to create an object in JavaScript: using the braces syntax (also known as the object literal syntax) and the Object
constructor.
The most popular implementation in JavaScript is to create one with braces. Let’s create a dictionary using traditional JavaScript objects, as shown in the following code snippet:
// Using the Object literal notation let dictionary2 = {}; // Using the built-in Object type let dictionary = new Object();
We can create a dictionary with the initial data as key-value pairs, using the object literal notation as follows:
let dictionary = { 'firstName': 'Gapur', 'lastName': 'Kassym', 'country': 'Kazakhstan' };
We created the dictionary
object with the key and value as string types. In the above code, we can remove quotes from keys as they are valid JavaScript identifiers:
let dictionary = { firstName: 'Gapur', lastName: 'Kassym', country: 'Kazakhstan' };
If you want to change an existing value or add a new value to the dictionary, you can set the new value by calling on the dictionary key, like so:
// Using the bracket notation dictionary['firstName'] = 'New Name'; // Using the property access dot notation dictionary.firstName = 'Tom';
We can access a specific value from the dictionary by directly calling the property name or indexer:
// Using bracket/indexer let firstName = dictionary['firstName']; // Using directly by property name via dot firstName = dictionary.firstName;
These are the basics of JavaScript Object
-based dictionaries. We can discuss more operations like iteration, deletion, etc. when we discuss how to implement TypeScript type-safe features.
Map
in JavaScriptThe Map
is JavaScript’s inbuilt dedicated dictionary object. It offers pre-developed instance methods to manipulate data saved inside the dictionary and also remembers the data insertion order. This map object lets you store JavaScript primitive types and any object type as key/value entities.
Learn how to create dictionaries in JavaScript using the Map
inbuilt object from the following sample code snippet:
// Creating a dictionary using Map const dictionary = new Map(); // Adding string data for a key-value pair dictionary.set('100', 'Hello World'); // Adding number-string data for a key-value pair dictionary.set(100, 'JavaScript'); // Adding boolean-string data for a key-value pair dictionary.set(true, 'TypeScript');
The Map
object stores any element as a key, unlike Object
, and thus, they return the two different values for the 100
number and '``100``'
string:
const str1 = dictionary.get('100'); console.log(str1); // Hello World const str2 = dictionary.get(100); console.log(str2); // JavaScript
To update values in the Map
dictionary, we should call the set
method by key:
dictionary.set('100', 'Hello TypeScript');
These are the basics of JavaScript Map
-based dictionaries. We can discuss more operations when we cover how to implement TypeScript type-safe features.
When we use the dictionary in TypeScript after previously using it in JavaScript, we’ll run into errors because TypeScript needs to know the data type of an object before it can be accessed.
This means we will not have problems with the following code in JavaScript, but we will have problems with it in TypeScript. Let’s take a look:
const dictionary = {}; console.log(dictionary.firstName);
The above code snippet triggers a TypeScript compilation error as follows:
Here, dictionary.lastName
returns undefined
in JavaScript, but in TypeScript, it will throw an error:
const dictionary = { firstName: 'Gapur' }; console.log(dictionary.firstName); // Gapur console.log(dictionary.lastName); // Property 'lastName' does not exist on type '{ firstName: string; }'
Sure, we can use type any
in our code, but why use TypeScript without type checking?
const dictionary: any = {}; dictionary.firstName = 'Gapur'; // Works dictionary.lastName = 'Kassym'; // Works
Let’s learn how to properly make type-safe dictionaries in TypeScript without preventing compiler errors with the generic any
type.
There are three ways to avoid type issues of JavaScript dictionaries in TypeScript.
With this approach, we define key-value types for the JavaScript Object
structure using index signatures or mapped types. Let’s create the dictionary with key
and value
as string types using an index signature:
const dictionary: { [key: string]: string } = {}; dictionary.firstName = 'Gapur'; // Works dictionary.lastName = 'Kassym'; // Works
This implements a type-safe dictionary with string key-value pairs. You can verify type safety by trying to add non-string types as key-value data:
dictionary.age = 100; dictionary.createdAt = new Date();
All these statements trigger compilation errors:
We can call the key
name whatever we want. For this example, I will name it key
.
We also can’t leave out the key name or use literal types, according to this syntax rule:
const dictionaryWithoutKeyName: { [string]: string } = {}; // Error const dictionaryWithLiteralTypes: { [key: 'firstName' | 'lastName']: string } = {}; // Error
Because we use plain JavaScript objects under the hood, we can store any data type in values and use only strings, numbers, and symbols as keys. Look at the following example that uses numbers as keys and a custom User
type for values:
type User = { firstName: string; lastName: string; } const dictionary: { [key: number]: User } = {}; // Create user with firstName and lastName dictionary[1] = { firstName: 'Gapur', lastName: 'Kassym' };
If we want to omit a property, we can use Partial<Type>
utils to do so:
type User = { firstName: string; lastName: string; } const dictionary: { [key: number]: User } = {}; const dictionaryWithPartial: { [key: number]: Partial<User> } = {}; // Works very well even if lastName is missing dictionaryWithPartial[1] = { firstName: 'Tom' }
Dictionary
typeWe don’t typically create one dictionary instance for the entire codebase — we’re usually creating multiple dictionaries, so creating a dictionary type, rather than repetitively writing the same index signature, is a more efficient approach.
You can create a new Dictionary
type with a preferred index signature as follows:
type Dictionary = { [key: string]: number; }; const dictionary: Dictionary = {}; dictionary['David'] = 100; dictionary['Mark'] = 120; dictionary['Ann'] = 120;
This lets you only store string-number data pairs, so you can use TypeScript generics to create dictionaries with pre-declared key-value data types:
type Dictionary<Key extends keyof any, Value> = { [key in Key]: Value; // Mapped types syntax }; const dictionary1: Dictionary<string, number> = {}; dictionary1['David'] = 100; dictionary1['Mark'] = 120; dictionary1['Ann'] = 120; const dictionary2: Dictionary<number, Date> = {}; dictionary2[100] = new Date(); dictionary2[200] = new Date(2024, 2, 20);
Here, we implement the generic Dictionary<Key, Value>
type to define dictionary types with pre-declared key-value types without writing repetitive code. Also, note that here we used the mapped types feature instead of index signatures because index signatures can’t work properly with generic types.
As we saw in previous examples, adding data to Object
-based dictionaries is possible with bracket and dot notation. The bracket notation is more suitable for these scenarios, as demonstrated in the following code snippet:
type User = { firstName: string; lastName: string; } const users: Dictionary<string, User> = {}; // Adding a new record users['A100'] = {firstName: 'Tom', lastName: 'Max'}; // Accessing value by key console.log(users['A100'].firstName); // Tom // Updating value by key users['A100'] = {firstName: 'David', lastName: 'Jones'};
If you need to create a read-only dictionary that only lets you read key-value pairs after initialization, you can wrap the Dictionary<Key, Value>
type with Readonly<Type>
:
const users: Readonly<Dictionary<string, User>> = { 'A100': { firstName: 'Tom', lastName: 'Max' } }; users['A100'] = {firstName: 'David', lastName: 'Jones'}; // Index signature in type 'Readonly<Dictionary<string, User>>' only permits reading
Also, it’s possible to restrict value object property modifications using the Readonly<Type>
type for value, as shown in the following code:
const users: Dictionary<string, Readonly<User>> = {}; users['A100'] = {firstName: 'David', lastName: 'Jones'}; users['A100'].firstName = 'Max'; // Error users['A100'].lastName = 'Ron'; // Error // Cannot assign to 'firstName'/'lastName' because it is a read-only property
You can check if a specific key exists in a dictionary using the hasOwnProperty()
instance method or the in
operator.
To delete keys from dictionaries, you can use the delete
operator. For example, the following increase(key)
function increases a specific value by 10 if the key
exists, otherwise, it initializes a new record with 1000
as the value:
const counts: Dictionary<string, number> = {}; function increase(key: string) { counts[key] = counts.hasOwnProperty(key) ? (counts[key] + 10) : 1000; } increase('A'); increase('B'); increase('A'); increase('A'); console.log(counts); // {A: 1020, B: 1000}
We can also use the in
operator:
function increase(key: string) { counts[key] = (key in counts) ? (counts[key] + 10) : 1000; }
JavaScript’s for…of
loop, array destructuring, and Object.entries()
features let us write shorthand code for iterating through dictionaries. The following code snippet extracts key-value data into two separate constants:
const counts: Dictionary<string, number> = { A: 1020, B: 1000, C: 1000 }; for(const [key, value] of Object.entries(counts)) { console.log(key, value); } // A 1020 // B 1000 // C 1000
If you only need either keys or values — not both at once — you can use Object.keys()
and Object.values()
functions as follows:
for(const key of Object.keys(counts)) { // Alternative: for (const key in counts) { console.log(key); } // A // B // C for(const value of Object.values(counts)) { console.log(value); } // 1020 // 1000 // 1000
TypeScript offers conditional type definition features for writing advanced type definitions based on developer-defined conditions. A conditional type is typically created using the JavaScript ternary operator syntax as follows:
type MyType = Ferrari extends Car ? number : string;
The above MyType
type becomes number
if the Ferrari
type/interface extends Car
, otherwise, MyType
becomes string
. This feature alone might not seem very useful for type-safe dictionaries, but we can combine this with mapped types to create advanced, type-safe dictionaries.
For example, you can create dictionary types productively by modifying an existing type or interface, as demonstrated in the following code:
type SensorStats = { distanceA: number; distanceB: number; weightA: number; patternA: Uint8Array; patternB: Uint8Array; }; type OptimizerDict<Type> = { [key in keyof Type as Type[key] extends number ? key : never]: boolean; }; const dictionary: OptimizerDict<SensorStats> = { distanceA: false, distanceB: true, weightA: false }; dictionary['distanceA'] = true; dictionary['weightA'] = true; console.log('patternA' in dictionary); // false
The above code snippet defines the OptimizerDict<Type>
dictionary type by using only number
type properties from the SensorStats
type and converting them to boolean
properties. Here, the dictionary
object lets you store only Boolean values with limited distanceA
, distanceB
, and weightA
keys.
This implementation looks a bit complex for TypeScript beginners, but learning it will offer more developer productivity than repetitively defining dictionary keys with string literal types.
Record<Keys, Type>
utilityEarlier, we discussed how to create our own Dictionary<Key, Value>
generic type to use JavaScript Object
as a type-safe dictionary. TypeScript 2.1 released the inbuilt Record<Keys, Type>
utility type with type-safe dictionary features to let developers productively create dictionaries, so you don’t need to define custom dictionary types if you prefer this inbuilt utility type:
const dictionary: Record<string, string> = {}; dictionary['firstName'] = 'Gapur'; dictionary['lastName'] = 'Kassym';
We can use string literal types to allow only pre-declared keys:
type UserFields = 'firstName' | 'lastName'; let dictionaryUnion: Record<UserFields, string> = { firstName: 'Tom', lastName: 'Jones' }; // Works dictionaryUnion = { firstName: 'Aidana', lastName: 'Kassym', location: 'London' // 'location' does not exist in type 'Record<UserFields, string>' };
For working with Record<Keys, Type>
, you can use the same dictionary handling techniques that we used in indexed object notation as both methods use JavaScript Object
under the hood.
Map
in TypeScriptWe discussed using Map
for creating dictionaries in JavaScript. TypeScript implements generic types for the inbuilt Map
object, so we can use Map
to create type-safe dictionaries in TypeScript, too. Let’s build a simple dictionary with the key as a string and value as a number in TypeScript:
const dictionary = new Map<string, number>(); dictionary.set('JavaScript', 4); // No Error dictionary.set('HTML', 4); // Works // Argument of type 'string' is not assignable to parameter of type 'number' dictionary.set('React', '4'); // Error
We can also use the key as the string literal type and the value as the object type:
type JobInfo = { language: string, workExperience: number, } type JobPosition = 'Frontend' | 'Backend'; const dictionary = new Map<JobPosition, JobInfo>(); dictionary.set('Frontend', { language: 'JavaScript', workExperience: 5 }); dictionary.set('Backend', { language: 'Python', workExperience: 4 });
Unlike the traditional Object
, Map
offers pre-developed instance methods to work with key-value data pairs. You can use the set()
method to add or update a value based on a given key:
const dictionary = new Map<string, number>(); // Add a new record dictionary.set('A', 10); // Update an existing record dictionary.set('A', 20);
You can use the get()
method to access a value based on a specific key:
const A = dictionary.get('A');
You can check whether a specific key exists on a dictionary with the has()
instance. For deleting dictionary keys, you can use the delete()
method.
The following code snippet implements the same increase()
function we’ve implemented before, but this time for a Map
object:
const counts = new Map<string, number>(); function increase(key: string) { counts.set(key, counts.has(key) ? (counts.get(key)! + 10) : 1000); } increase('A'); increase('B'); increase('A'); increase('A'); console.log(counts); // {A: 1020, B: 1000}
The inbuilt clear()
method helps developers remove all dictionary keys at once.
The Map
standard object implements the iterable protocol, so you can directly use the for…of
loop with array destructuring to extract key-value pairs at once, as demonstrated below:
const counts = new Map<string, number>([ ['A', 1020], ['B', 1000], ['C', 1000] ]); for(const [key, value] of counts) { console.log(key, value); } // A 1020 // B 1000 // C 1000
If you only need either keys or values — not both at once — you can use keys()
and values()
methods as follows:
for(const key of counts.keys()) { console.log(key); } // A // B // C for(const value of counts.values()) { console.log(value); } // 1020 // 1000 // 1000
How to best build a type-safe dictionary depends on your use case and preference. If you would like to use JavaScript Object
as the underlying dictionary object and use Object
-based APIs in dictionary operations, you can either build your own Dictionary<Key, Value>
type with indexed object notation or use the inbuilt Record<Keys, Type>
utility type.
The Map
object offers a modern way to work with dictionaries with pre-developed dedicated data-handling methods, and easy iteration syntax via iterable protocol. If you want to avoid old fashioned JavaScript syntax such as delete
and in
, and prefer a more standard method, the Map
object is a good choice for creating type-safe dictionaries.
Select the type-safe dictionary creation method that benefits your use case and preference. Thanks for reading and happy coding!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.