TypeScript String Enums

TypeScript String Enums Header

Derived from photo by Markus Spiske / raumrot.com, CC-BY

This post expands upon a popular issue/proposal in the TypeScript GitHub repository called 'String enums'. The proposal is to create an enum that uses string values as opposed to the currently used numeric values. After TypeScript 2.1 was released, user wallverb posted an interesting solution leveraging the new features – many thanks for the inspiration.

While the term pattern might be a bit of an oversell, this technique or trick will get you the intended result. Included is an example to demonstrate the usefulness of this solution. The article compares other TypeScript features that don't quite meet the mark and also discusses how the solution works to create the right combination of type checking and runtime behavior.

The TypeScript team might still make it easier to create 'string enums' in future versions of the language. Regardless, learning how this solution works will deepen your understanding of the new TypeScript 2.1 features.

Use Case

If you think about inputs such as dropdowns or radio buttons where the user must select a single value from multiple choices, the underlying values oftentimes map nicely to an enum data structure. For example, consider a selection of shirt sizes. You could easily define the shirt sizes with an enum:

enum Size {
    XLarge,
    Large,
    Medium,
    Small
}

This is a nice data structure with which to code. All of the related values are in one place and it's easy to access a value from the list.

To understand what TypeScript is doing, it compiles the enum to a data structure at runtime that looks like this:

var Size;
(function (Size) {
    Size[Size["XLarge"] = 0] = "XLarge";
    Size[Size["Large"] = 1] = "Large";
    Size[Size["Medium"] = 2] = "Medium";
    Size[Size["Small"] = 3] = "Small";
})(Size || (Size = {}));

While this structure is a bit hard to follow, it allows for the following runtime behavior:

    console.log(Size.XLarge); // 0
    console.log(Size.Medium); // 2
    console.log(Size[Size.Small]); // 'Small'

Looking at the first two lines, you see the underlying values for these enum choices are numbers. However in the last line, you see it is possible to get the enum selection as a string.

If you define a property that accepts one of the enum values as a string, the type of that property will be string which has no association with the enum from which you would like to get the value.

interface Shirt {
    size: string;
}

// all is right with the world
const shirt: Shirt = {
    size: Size[Size.Large] 
};

// what happened to the enum values??
const invalidShirt: Shirt = {
    size: 'ExtraMedium'
};

TypeScript, however does offer a way to limit string values but this is a separate type definition altogether:

 type SizeString = 'XLarge' | 'Large' | 'Medium' | 'Small';
 const good: SizeString = 'Large'; // compiles
 const bad: SizeString = '' // doesn't compile

Now think back to the multi-choice selections in HTML like the dropdown and radio buttons. The HTML elements define values using attributes which are strings. So wouldn't it be great if you could get the simplicity of an enum but with string values instead of numeric values?

The Code

The SizeSelectComponent demonstrates this use case. Its template contains a select element with several options defined, in this case it displays a range of shirt sizes. Notice the Size variable and the Size type defined. These declarations create the 'string enum' definition.

import { Component, Input } from '@angular/core';

const Size = {
  XLarge: 'xl' as 'xl',
  Large: 'l' as 'l',
  Medium: 'm' as 'm',
  Small: 's' as 's'
}
type Size = (typeof Size)[keyof typeof Size];
export { Size };

@Component({
    moduleId: module.id,
    selector: 'app-size-select',
    template: `
        <select [ngModel]="selectedSize">
            <option value="{{size.XLarge}}">Extra Large</option>
            <option value="{{size.Large}}">Large</option>
            <option value="{{size.Medium}}">Medium</option>
            <option value="{{size.Small}}">Small</option>
        </select>    
    `
})
export class SizeSelectComponent { 
    @Input() selectedSize: Size;

    // Bonus - see how the constants define 
    // the values in the markup above
    size = Size;
}

Furthermore, notice the selectedSize input property for this component. Notice it is of type Size. Now when consuming this component, you can set the selectedSize property using the Size variable properties.

import { Component, OnInit } from '@angular/core';

import { Size } from './app.size-select';

@Component({
  selector: 'my-app',
  template: `<app-size-select [selectedSize]="size"></app-size-select>`,
})
export class AppComponent implements OnInit {
    size: Size;

    ngOnInit() {
        // Looks like an enum, but the value is a string
        this.size = Size.Small;
    }
 }

That's the scenario. It looks very similar to an enum but an enum used in this way would pass an integer value. This provides a string value.

Note: Because this example uses the ngModel directive, be sure to import the FormsModule into the NgModule for these components.

What's Going On?

Take a look at the Size declarations again:

const Size = {
  XLarge: 'xl' as 'xl',
  Large: 'l' as 'l',
  Medium: 'm' as 'm',
  Small: 's' as 's'
}
type Size = (typeof Size)[keyof typeof Size];

The first thing to note is that there are two declarations. The first is the variable declaration which assigns all of the string values to a simple object literal. There is one added compile-time feature here where the properties are defined with their respective string literal type. By adding the as 'xl' type when setting the property XLarge: 'xl' as 'xl', TypeScript performs a compile-time check to prevent overwriting Size.XLarge with a different string value.

The second declaration is the type declaration:

type Size = (typeof Size)[keyof typeof Size];

This declaration is easier to read rewritten into two statements:

type SizeLiteral = typeof Size;
type Size = SizeLiteral[keyof SizeLiteral];

The only thing that has changed is that typeof Size is assigned to its own type called SizeLiteral which is then re-used in the Size type declaration. TypeScript uses the typeof operator to create a type from the Size object definition.

The second line introduces the keyof T operator. TypeScript uses keyof T known as the index type query operator to create a string literal type from the properties of an existing type. For instance keyof SizeLiteral is equivalent to the string literal type 'XLarge' | 'Large' | 'Medium' | 'Small'. This is nice but it isn't going to provide the right type for this scenario, so the next level to this is called the indexed access operator.

Whereas the index type query operator pulls the string literals type from the properties of a given type, the indexed access operator, also represented as T[K], pulls the underlying types of those properties. So remember how you used string literals to define the types of the Size declaration such as XLarge: 'xl' as 'xl'? The Size type is equivalent to the string literal type 'xl' | 'l' | 'm' | 's' because those are the types of the Size type's properties.

Now to return to the initial implementation, SizeLiteral is removed in place of typeof Size and wrapped in parentheses to represent T in the T[K], index type query operator:

type Size = (typeof Size)[keyof typeof Size];

Finally, both the variable and type are exported as Size so that the parent component can consume this interface:

export { Size };

Wrapping Up

You saw how to leverage the new TypeScript 2.1 features to create a type that makes working with string literals much like working with an enum. Don't worry if you don't understand this the first time. There is a lot here so you may consider reading the post again and trying it out yourself. For more information on advanced types in TypeScript, take a look at the documentation.

Do you find this technique useful? In what other scenarios would you consider using this solution? Please leave a note in the comments.

One thought on “TypeScript String Enums”

  1. Great post!
    Mapped types (with index typex) looks like one of the best features added to Typescript recently. I wonder how many other interesting scenarios people will discover for this feature.

Leave a Reply

Your email address will not be published. Required fields are marked *