Adventures in TypeScript Typing

This week I had the need to create a rather simple object that I wanted to be properly typed. This object consists of a couple of enums that will be categories with sub-categories that contain an object. This is a simple enough structure really:

const categories = {
  category1: {
    sub-category: {...}
  },
  category2: {
    sub-category: {...}
  }
}

So, let’s type this out shall we? First we need to define the object that will be the value of the sub-categories.

type ProcessProps = {
  cardLabel: string,
  processName: string,
  actionRequired: boolean
}

Next up will be the category and sub-category enums.

enum Categories {
  CAT1,
  CAT2
}

enum SubCategories {
  SUB1_CAT1,
  SUB1_CAT2,
  SUB2_CAT1,
  SUB2_CAT2
}
  

Now, we need to define our final object structure.

type CategorizedObject = {
  [key in Categories]: SubCategory
}

type SubCategory = {
  [key in SubCategories]: ProcessProps
}

And finally implement it.

const categories: CategorizedObject = {
    [Categories.CAT1]: {
        [SubCategories.SUB_CAT1]: {
            cardLabel: 'Some Label',
            processName: 'Some Process',
            actionRequired: true
        }
    }
}

OK, this all seems kosher and fairly straight forward. However, we’ve got an error on the ‘Categories’ key. This error reads:

Type of computed property’s value is ‘{ [SubCategories.SUB_CAT1]: { cardLabel: string; processName: string; actionRequired: true; }; }’, which is not assignable to type ‘SubCategory’.
Property ‘1’ is missing in type ‘{ [SubCategories.SUB_CAT1]: { cardLabel: string; processName: string; actionRequired: true; }; }’ but required in type ‘SubCategory’.(2418)

input.ts(17, 26): The expected type comes from property ‘0’ which is declared here on type ‘CategorizedObject’

While the error does tell me what the problem is, when frustrated or in a hurry I failed to read all of it. Embarrassing enough I spent hours on this. But here’s the problem “Property ‘1’ is missing in type…”.

What that’s saying is that you have to include all the keys from the enums in their implementation. For the ‘Categories’ level, that is the behavior we’re looking for. But not so much for the sub categories. So, how do we fix this issue?

We could create a separate enum for each category. But that would take a bit of time and wouldn’t provide that much benefit. The simplest solution is to simply make the SubCategory keys optional. So we change our ‘SubCategory’ type to this:

type SubCategory = {
  [key in SubCategories]?: ProcessProps;
}

This small change now allows us to define a category that doesn’t require all the sub-category keys to be used. Which will look something like:

const categories: CategorizedObject = {
    [Categories.CAT1]: {
        [SubCategories.SUB1_CAT1]: {
            cardLabel: 'Some Label',
            processName: 'Some Process',
            actionRequired: true
        },
        [SubCategories.SUB1_CAT2]: {
            cardLabel: 'Some Label',
            processName: 'Some Process',
            actionRequired: false
        }
    },
    [Categories.CAT2]: {
        [SubCategories.SUB2_CAT1]: {
            cardLabel: 'Some other Label',
            processName: 'Some Other Process',
            actionRequired: false
        },
        [SubCategories.SUB2_CAT2]: {
            cardLabel: 'Some other Label',
            processName: 'Some Other Process',
            actionRequired: true
        }
    }
}

While implementing this sort of typing may seem menial, it provides such a better developer experience. It also prevents errors when using the ‘categories’ constant elsewhere in the app. Another benefit of going to the trouble of implementing types is developer training. When a new person comes on board, the person just learning the app can just click through the variables and learn the application’s structure. Hopefully you get to learn a bit from my frustration.

Until next time, happy coding.

Share This: