Nested Enums in TypeScript.

How to solve the need of nested and multi-level Enums in TypeScript.

TypeScript Logo
TypeScript Logo

Nested/Multi-level Enums in TypeScript

I was recently working on the refactoring of the naming of the entries set by our application in the Cookies and LocalStorage. Every name was kind of different, and we wanted to ensure a common naming pattern for all the Cookies and LocalStorage items that our Front-end was setting in the application.

So the first thing that I thought was using an Enum to store all the naming there and easily access them. Unfortunately, it turned out that Enums are actually not that flexible. Meaning that you can't have nested or multi-level Enums.

And I really wanted to have a nestable data structure: I didn't want multiple Enums holding the names (like TechnicalCookies.country or AnalyticsCookies.userId), but I wanted a single Enum as the sole entry point to access them all (like CookieKeys.TechnicalCookies.country and CookieKeys.AnalyticsCookies.userId).

This is also great working with auto-complete from the IDE, meaning that just typing CookieKeys. you can explore all the different Cookie keys available without having to find them somewhere else in the source code.

I also wanted to add some namespacing, so that every name looked kind of the same and was easily recognizable. So here I stumbled upon another problem: you can't have computed string properties in an Enum (see code below).

//String Enums can't have computed properties
    export enum AppNamespaces {
        COOKIES = 'MyAppCookies'
    }
    
    export enum TechnicalCookies {
        COUNTRY = `${AppNamespaces.COOKIES}_country`, //Error! emoji 🔥 → Only numeric enums can have computed members, but this expression has type 'string'.
    }

So it seemed that Enums were not the right tool to accomplish what I was aiming for.

My Solution emoji 🔬

So after some research and many trials, I came up with a solution and some working code. For simplicity let's just focus on the Cookies (for the LocalStorage is actually the exact same concept, so there is no need to show it, and it easily applies to every other similar situation other than Cookies and LocalStorage names).

I used objects (fixed as constants) so that I could easily nest them. Then I extracted the computed values, so that I could use them as a CookieKey type, and ensure type-safety across every little piece of the codebase that needed to interact with Cookies.

//cookieUtils.ts
    import { CookieKey } from "./appNamespaces";

    export function setCookie(name: CookieKey, value: string) {
        // @TODO: Use a library here ;)
    }

The code above has a very importante feature: it's type safe! That means that no one could just put a random parameter as name, but he would be forced to provide a valid name. This avoids typos and incorrect-name errors. The CookieKeys type should be used in every place where we know that we are dealing with a Cookie name.

//typeScriptUtils.ts
    export type ValuesOf<T> = T[keyof T];
//appNamespaces.ts
    import { ValuesOf } from "./typeScriptUtils";

    export enum AppNamespaces {
        COOKIES = 'MyAppCookies', //Dummy names: @TODO find better naming
        LOCAL_STORAGE = 'MyAppLocalStorage',
        SESSION_STORAGE = 'MyAppSessionStorage'
    }

    const TechnicalCookies = {
        country: `${AppNamespaces.COOKIES}_country`,
        language: `${AppNamespaces.COOKIES}_language`,
        userType: `${AppNamespaces.COOKIES}_user_type`
    } as const;

    const AnalyticsCookies = {
        userId: `${AppNamespaces.COOKIES}_user_id`,
    } as const;

    export const CookieKeys = {
        TechnicalCookies,
        AnalyticsCookies
    }

    export type CookieKey = ValuesOf<typeof TechnicalCookies> | ValuesOf<typeof AnalyticsCookies>;

Here we can finally combine our code:

//app.ts
    import { AppNamespaces, CookieKeys } from "./appNamespaces";
    import { setCookie } from "./cookieUtils";

    setCookie('country', 'ch'); //Error! emoji 🔥 → Argument of type '"country"' is not assignable to parameter of type 'CookieKey'.
    setCookie(`${AppNamespaces.COOKIES}_country`, 'ch'); //Works! emoji 🎉 → it's type-safe!
    setCookie(CookieKeys.TechnicalCookies.country, 'ch'); //Works! emoji 🎉emoji 🎉 → it's type-safe and has auto-complete!

I hope this can help someone who may experience a similar problem.

Although I think this is a good solution, I'm still on my way to master TypeScript, and I'm very open and happy to hear in case there were ways to improve and make it better!

For updates, insights or suggestions, feel free to post a comment below! emoji 🙂


Responses

Latest from Web Engineering browse all

WebAssembly + Emscripten Notes. | cover picture
WebAssembly + Emscripten Notes.
Created December 25 2020, updated August 19 2021.
My notes on learning WebAssembly and its integration with Emscripten.
Docker Notes. | cover picture
Docker Notes.
Created November 19 2020, updated December 25 2020.
My notes on learning Docker.
Algorithms in JavaScript: Bubblesort, Quicksort, Mergesort. | cover picture
Algorithms in JavaScript: Bubblesort, Quicksort, Mergesort.
Created September 17 2020, updated December 25 2020.
Explanation and implementation using JavaScript of some of the most popular sorting algorithms: Bubblesort, Quicksort and Mergesort.
×