DEV Community

loading...
Angular

Managing Key-Value Constants in TypeScript

Suguru Inatomi
Google Developers Expert for Angular / Lead of Angular Japan User Group
・2 min read

A lot of applications have a dropdown select menu in a form. Let's imagine a form control like below;

Demo

Typically, each select menu's item has ID and label. The ID is responsible to communicate with other components, services, or server-side. The label is responsible to display text for users.

This post explains how to manage constants for the menu items which has ID and mapping for its label. It uses TypeScript's as const feature which is introduced since v3.4.

Define colorIDs Tuple

In TypeScript, a tuple is an array, but its length and items are fixed. You can define a tuple with as const directive on the array literal. (as const directive needs TypeScript 3.4+)

Create colors.ts and define colorIDs tuple as following;

export const colorIDs = ['green', 'red', 'blue'] as const;
Enter fullscreen mode Exit fullscreen mode

The type of colorIDs is not string[] but ['green', 'red', 'blue'] . Its length is absolutely 3 and colorIDs[0] is always 'green'. This is a tuple!

Extract ColorID Type

A Tuple type can be converted to its item's union type. In this case, you can get 'green' | 'red' | 'blue' type from the tuple.

Add a line to colors.ts like below;

export const colorIDs = ['green', 'red', 'blue'] as const;

type ColorID = typeof colorIDs[number]; // === 'green' | 'red' | 'blue'
Enter fullscreen mode Exit fullscreen mode

Got confusing? Don't worry. It's not magic.

colorIDs[number] means "fields which can be access by number", which are 'green' , 'red', or 'blue' .

So typeof colorIDs[number] becomes the union type 'green' | 'red' | 'blue'.

Define colorLabels map

colorLabels map is an object like the below;

const colorLabels = {
  blue: 'Blue',
  green: 'Green',
  red: 'Red',
};
Enter fullscreen mode Exit fullscreen mode

Because colorLabels has no explicit type, you cannot notice even if you missed to define red 's label.

Let's make sure that colorLabels has a complete label set of all colors! ColorID can help it.

TypeScript gives us Record type to define Key-Value map object. The key is ColorID and the value is string. So colorLabels 's type should be Record<ColorID, string> .

export const colorIDs = ['green', 'red', 'blue'] as const;

type ColorID = typeof colorIDs[number];

export const colorLabels: Record<ColorID, string> = {
  green: 'Green',
  red: 'Red',
  blue: 'Blue',
} as const;
Enter fullscreen mode Exit fullscreen mode

When you missed to define red field, TypeScript compiler throw the error on the object.

Compiler Error

By the way, Angular v8.0+ is compatible with TypeScript v3.4. The demo app in the above is the following;

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

import { colorIDs, colorLabels } from './colors';

@Component({
  selector: 'app-root',
  template: `
    <label for="favoriteColor">Select Favorite Color:&nbsp;</label>
    <select id="favoriteColor" [formControl]="favoriteColorControl">
      <option *ngFor="let id of colorIDs" [ngValue]="id">
        {{ colorLabels[id] }}
      </option>
    </select>
    <div>Selected color ID: {{ favoriteColorControl.value }}</div>
  `,
})
export class AppComponent {
  readonly colorIDs = colorIDs;
  readonly colorLabels = colorLabels;

  readonly favoriteColorControl = new FormControl(this.colorIDs[0]);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • as const turns an array into a tuple
  • typeof colorIDs[number] returns an union type of its item
  • Define an object with Record<ColorID, string> for keeping a complete field set.

Discussion (7)

Collapse
michaeljota profile image
Michael De Abreu • Edited

What is advantage between this and a string enum?

export enum Colors {
  blue = 'Blue',
  green = 'Green',
  red = 'Red',
}

String enums are transformed to regular objects, so you could access them as Colors.blue, just like in your example.

Collapse
lacolaco profile image
Suguru Inatomi Author

As far as I know, there isn't an advantage technically. But I prefer union types because enum is not an ECMAScript's standard syntax.
After transpilation, output code with a tuple and an object is similar to the original TS code, but output code with enum is different from the original TS code a lot.
I think a big value of TypeScript is the mindset; it is a superset of ECMAScript. So I don't want to use enum.

Collapse
woodywoodsta profile image
Sean Wood

I hear what you're saying! But that's similar to buying a bigger car and never putting anything in the extra space because it wasn't available in your standard car.

I think that the choice of adopting Typescript as a language comes with the acceptance of a build step, the outcome of which is up to the compiler.

Collapse
michaeljota profile image
Michael De Abreu

As long as you know what it becomes, I always suggest the usage of the languages features. Whatever they are.

Collapse
constjs profile image
Piotr Lewandowski • Edited

Nice replacements for enums.
Example can be improved a bit to avoid duplications (everything is calculated based on colorLabels):

export const colorLabels = {
  green: 'Green',
  red: 'Red',
  blue: 'Blue',
} as const;

type ColorID = keyof typeof colorLabels;

export const colorIDs = Object.keys(colorLabels) as ColorID[];
Collapse
lacolaco profile image
Suguru Inatomi Author

Thank you! I agree on it is simpler than I posted version.
In other hand, I think the ordering is important in this usecase. Object fields is easily sorted by code editing so it is not safe to keep the ordering.
This is why I want to manage IDs as a tuple. How do you think?

Collapse
briancodes profile image
Brian • Edited

Object.keys(colorLabels) keeps the order that the keys were added to the object. The object can't be edited after it's created as you've used const ... as const, so I think it's safe