How to eliminate "magic strings" with a fancy Typescript trick 
Tags: typescript, webdev, javascript, tutorial
Published: 2020-10-01 21:11:23

One of the first things you learn as a professional developer is that magic strings are bad. Why? 🤔 Because typos stink and can really ruin an application in a way that's hard to track down.

For instance, my team and I were use Storybook to display all of the UI widgets that we have available in our library. And there were a couple of times where we misspelled something only to find that one of the widgets was missing from the final output.

# The Ideal Goal

Ideally a developer wouldn't have to remember the full path or the agreed upon folder names for storybook. If we rely on memory, they're going to type things wrong.

# The Solution?

We'll create a function that lets developers "crawl" the paths to create a concatenated path.

For instance, if we wanted the folder structure in the image below, I'll show you how we would initialize the title.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9ifr172e47rabu5g6jwu.png)

That would mean inside the `dateAndTimePicker.stories.tsx` file, I would initialize the title like this:
```ts
const title = makeTitle('Widgets')('Temporal')('Date Range Picker').finalize();
```

The coolest part of this approach is that you can't spell "Widgets" wrong... Typescript won't let you. And you also get Intellisense to help you remember what options are allowed.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9xodxkox4vz4vmwunfdu.png)

Let's show how to make this possible.

#Step 1: Store the hierarchy  in a series of nested objects

This first step is basically the same as how you would eliminate "magic strings" in any project-- by creating constants. But the cool part here is that Typescript naturally lets you store a hierarchy of constants as a series of nested objects. You'll see the power that gives us later when we start to use `keyof` to create smarter types.

```ts
const headingsMapObj = {
    Widgets: {
        Temporal: {
            'Date Range Picker': 'Date Range Picker',
        },
        Input: 'Input',
        Checkbox: {
            'Single Checkbox': 'Single Checkbox',
            'Checkbox Group': 'Checkbox Group'
        }
    },
    'An Introduction': {
        Welcome: 'Welcome',
    },
    Patterns: {
        Spacing: 'Spacing',
        Flow: 'Flow'
    },
} as const;
```
#Step 2: Turn this into a type
```ts
type HeadingsMap = typeof headingsMapObj;
```

# Step 3: Create the path builder

```ts
const makeTitle = <K1 extends keyof HeadingsMap>(level1: K1) => {
    const paths: string[] = [];
    function pushKeyIfStringOrThrow(input: string | number | symbol){
        if(typeof input === 'string'){
            paths.push(input);
        } else {
            throw new TypeError(`Unsupported type: ${typeof input}`)
        }
    }

    const finalize = () => {
        return paths.join(' / ');
    };

    pushKeyIfStringOrThrow(level1)

    const builderFnLevel2 = <K2 extends keyof HeadingsMap[K1]>(level2: K2) => {
        pushKeyIfStringOrThrow(level2)
        const builderFnLevel3 = <K3 extends keyof HeadingsMap[K1][K2]>(level3: K3) => {
            pushKeyIfStringOrThrow(level3)
            const builderFnLevel4 = <K4 extends keyof HeadingsMap[K1][K2][K3]>(level3: K4) => {
                pushKeyIfStringOrThrow(level3)
                return {
                    finalize
                };
            };
            builderFnLevel4.finalize = finalize;
            return builderFnLevel4;
        };
        builderFnLevel3.finalize = finalize;
        return builderFnLevel3;
    };
    builderFnLevel2.finalize = finalize;
    return builderFnLevel2;
};
```

And that's it! :)

# Okay, cool... but how does that work?

It would probably take a while to explain how and why that works. And I'll be honest, it's taken me a long time working with Typescript to create something this wild. And if you're interested in a detailed breakdown of how the above code works, reach out in the comments and I'll create a follow up post.

But the basic idea is that the `keyof` type operator creates a more strict type that is more narrow than a string.

So in the case of a type like:

```ts
const exampleObj = {
    'hello': 'Bill',
    'goodbye': 'Ted'
} as const;
```
We can create a type that only allows `'hello' | 'goodbye'` by writing:
```ts
type exampleKeys = keyof typeof exampleObj;
```

_Here's the next bit of magic._ Let's say we wanted to get a type that was only `'Bill' | 'Ted'`.

All we'd have to do is write:
```ts
type Example = typeof exampleObj;
type ExampleValues = Example[keyof Example];
```
Note: if `ExampleValues` is still `string` when you hover over it, you might have forgotten to add `as const` to the end of the `exampleObj` instantiation. Another dev.to user has [a great explanation of what makes `as const` work](https://dev.to/tkudlinski/as-const-in-typescript-5390).

#Wrapping Up

Thanks for coming along in this brief journey on why Typescript is so fun and how it can solve unique code problems that no other language can solve. :)