Building a type-safe dictionary in TypeScript - LogRocket Blog (2023)

A dictionary is a common data structure in a programming language. As JavaScript developers, it’s important to work with type-safe dictionaries because there will be conflicts or unexpected issues if we store different types of data within the same dictionary.

In this article, we’ll cover how to build a type-safe dictionary using TypeScript.

What is a type-safe dictionary?

First of all, we need to know what a dictionary is. In a programming language, a dictionary is a typical data structure that stores data in key-value pairs. JavaScript, however, does not offer a Dictionary type. Thankfully, we can create a type-safe dictionary in a few simple ways.

Using Object types in JavaScript

There are two primary ways to create a dictionary in JavaScript: using the Object type and using key-value pairs. The most popular implementation in JavaScript is to create one with the Object type. Let’s create a dictionary using the following code:

// Using the built-in Objectlet dictionaryViaObject = new Object();// Using the Object literal notationlet dictionaryViaLiteral = {};

We can create a dictionary with the initial data as key-value pairs:

let dictionaryViaLiteral = { 'firstName': 'Gapur', 'lastName': 'Kassym', 'country': 'Kazakhstan'};

We created the dictionaryViaLiteral dictionary with the key and value as string types.

If you want to change or add the value to the dictionary, you can set the new value by calling on the dictionary key, like so:

// Using bracketdictionaryViaLiteral['firstName'] = 'New Name';// Using directly by property name via dotdictionaryViaLiteral.firstName = 'Tom';

We can access the value from the dictionary by directly calling the property name or indexer:

(Video) LogRocket TypeScript Meetup: Write more readable code with TS 4.4

// Using bracket/indexerconst firstName = dictionaryViaLiteral['firstName'];// Using directly by property name via dotconst firstName = dictionaryViaLiteral.firstName;

Using Map in JavaScript

A Map is a collection of key-value pairs, just like an object. The main difference is that Map allows you to use keys and values of any type. The Map provides amazing instance methods to manipulate with a dictionary. If you are interested, you can read more here.

// Using Mapconst dictionaryViaMap = new Map();// Add value using string keydictionaryViaMap.set("1", "string1");// Add value using number keydictionaryViaMap.set(1, "number1"); // Add value using boolean keydictionaryViaMap.set(true, "boolean1");

The Map stores value by any type of key, and thus, they return the two different values:

// Return string1const string1 = dictionaryViaMap.get('1');// Return number1const number1 = dictionaryViaMap.get(1);

In order to update values in the Map dictionary, we should call set method by key:

// Update the valuedictionaryViaMap.set('1', 'updatedString1');

Dictionary type errors in TypeScript

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 = {};dictionary.firstName; // Property 'firstName' does not exist on type '{}'

Here, dictionary.lastName returns undefined in JavaScript, but in TypeScript, it will throw an error.

const dictionary = { firstName: 'Gapur' };// Return the firstName Gapurdictionary.firstName;// Property 'lastName' does not exist on type '{ firstName: string; }'dictionary.lastName;

Sure, we can use type any in our code, but why use TypeScript without type checking?

(Video) LogRocket Meetup: Intro to useState in React

const dictionary: any = {};dictionary.firstName = 'Gapur'; // It worksdictionary.lastName = 'Kassym'; // It works

Building a type-safe dictionary in TypeScript

There are three ways to avoid type issues in TypeScript.

1. Using indexed object notation

We can check the type of data by using indexed object notation. Let’s create the dictionary with key and value as string types:

const dictionary: { [key: string]: string } = {};dictionary.firstName = 'Gapur'; // It works very welldictionary.lastName = true; // Type 'boolean' is not assignable to type 'string'

We can call key name whatever we want. For this example, I would like to name it key.

Over 200k developers use LogRocket to create better digital experiencesLearn more →

Also, we can’t leave out the key name or use union types, according to the syntax rule.

// 'string' only refers to a type, but is being used as a value here.const dictionaryWithoutKeyName: { [string]: string } = {}; // Errordictionary.firstName = 'Gapur';// An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type insteadconst dictionaryWithUnionType: { [key: 'firstName' | 'lastName']: string } = {}; // Errordictionary.firstName = 'Tom';

Let’s complicate our example:

(Video) LogRocket Meetup: Understanding API data-fetching methods in React

type User = { firstName: string; lastName: string;}const dictionary: { [key: number]: User } = {};// Create user with firstName and lastNamedictionary[1] = { firstName: 'Gapur', lastName: 'Kassym'};// We can't add location property because User type doens't exist in the locationdictionary[2] = { firstName: 'Tom', lastName: 'Jones', location: 'London', // Type '{ firstName: string; lastName: string; location: string; }' is not assignable to type 'User'.}

If we want to omit a property, we can use Partial utils for that.

type User = { firstName: string; lastName: string;}const dictionary: { [key: number]: User } = {};// Property 'lastName' is missing in type '{ firstName: string; }' but required in type 'User'.dictionary[1] = { firstName: 'Gapur',}; // Errorconst dictionaryWithPartial: { [key: number]: Partial<User> } = {};// Works very welldictionaryWithPartial[1] = { firstName: 'Tom'}

2. Using the Record<Keys, Type> utility

The Record<Keys, Type> is a TypeScript utility for creating key-value objects. It is a great choice if you want to create a key type as unions or enums.

const dictionary: Record<string, string> = {};dictionary.firstName = 'Gapur';dictionary.lastName = 'Kassym';

Let’s use a union type:

type UserFields = 'firstName' | 'lastName';let dictionaryUnion: Record<UserFields, string> = { firstName: 'Tom', lastName: 'Jones'}; // Works very welldictionaryUnion = { firstName: 'Aidana', lastName: 'Kassym', location: 'London' // Type is not assignable to type 'Record<UserFields, string>'}; // Error

3. Using Map in TypeScript

We discussed using Map for creating type-safe dictionaries in JavaScript. 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 Errordictionary.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 union 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});

Conclusion

How to best build a type-safe dictionary depends, as usual, on your use-case. If you want to build a simple dictionary, you can use the indexed object notation. If you work with the unions, enums, or more complex data, employing the Record<Keys, Type> util is best.

In general, using Map is a great practical way to solve type-safe issues with various types of data.

(Video) LogRocket URQL meetup: GraphQL, the URQL library, & building advanced features on the client-side

Thanks for reading. I hope you found this piece useful. Happy coding!

LogRocket: Full visibility into your web and mobile apps

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.

Try it for free.

(Video) TypeScript testing tools: Scaling the testing pyramid in TypeScript

Videos

1. LogRocket Meetup: How to start a blog using Docusaurus, GitHub Actions, and Azure Static Web Apps
(LogRocket)
2. LogRocket React Meetup: React Testing Overview - January 2022
(LogRocket)
3. Three.js game tutorial | LogRocket Blog
(LogRocket)
4. LogRocket GraphQL Meetup: Rapid development with GraphQL Codegen
(LogRocket)
5. LogRocket React Meetup: React Hooks For Infinite Scroll
(LogRocket)
6. LogRocket React Meetup: Exploring React Suspense With React Freeze
(LogRocket)
Top Articles
Latest Posts
Article information

Author: Rev. Leonie Wyman

Last Updated: 18/02/2023

Views: 6751

Rating: 4.9 / 5 (79 voted)

Reviews: 86% of readers found this page helpful

Author information

Name: Rev. Leonie Wyman

Birthday: 1993-07-01

Address: Suite 763 6272 Lang Bypass, New Xochitlport, VT 72704-3308

Phone: +22014484519944

Job: Banking Officer

Hobby: Sailing, Gaming, Basketball, Calligraphy, Mycology, Astronomy, Juggling

Introduction: My name is Rev. Leonie Wyman, I am a colorful, tasty, splendid, fair, witty, gorgeous, splendid person who loves writing and wants to share my knowledge and understanding with you.