Skip to main content

Prefer Explicit Maps


Excerpts from Prefer Explicit Maps | Kyle Shevlin by Kyle Shevlin below… 🔖

This tip is a bit of a weird one. I genuinely don’t think others will like it, but I hope you’ll hear me out. At the very least, it might make you think a bit.

I’m a big fan of enumerated states. In TypeScript, a great way to represent this is a union of strings. Let’s start with a basic one.

type Theme = 'light' | 'dark'

It might be tempting to make this a boolean, given that there are only two states, but bear with me.

Now, let’s make a ThemeContext we could use throughout our app.

const ThemeContext = React.createContext<Theme>('light')

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = React.useState<Theme>('light')

  return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
}

So far, so good. Now, let’s add a toggleTheme function to the context and export that. We’ll need to make some adjustments.

type ThemeContextValue = {
  theme: Theme
  toggleTheme: () => void
}

// ...

function ThemeProvider({ children }: { children: React.ReactNode }) {
  // ...

  const toggleTheme = React.useCallback(() => {
    // Here's the line to pay attention to
    setTheme(currentTheme === 'light' ? 'dark' : 'light')
  })

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

Do you see this line?

setTheme(currentTheme === 'light' ? 'dark' : 'light')

To use the parlance of the kids, I lowkey hate this line.

I’m not opposed to ternaries, but not all ternaries are the same. Some ternaries represent legitimate binary choices: if this, else that. But some ternaries are really “implicit maps”.

A “map” is a data structure that pairs keys to values with a one-to-one relationship. This key always gives me this value. For example, we could create a THEME_LABEL_MAP if we needed to associate strings with our themes, like so:

const THEME_LABEL_MAP: Record<Theme, string> = {
  dark: 'Dark',
  light: 'Light',
}

You can see we’ve established a one-to-one relationship between the key, a Theme, and a string. Our ternary from above does this, too, but not explicitly. If you squint hard enough, you can maybe see the NEXT_THEME map hidden in there.

const NEXT_THEME: Record<Theme, Theme> = {
  dark: 'light',
  light: 'dark',
}

And this is actually how I prefer to write this code. Now our toggleTheme function will look a bit different:

const toggleTheme = () => {
  setTheme(currentTheme => NEXT_THEME[currentTheme])
}

Doesn’t that read nicely? Our map is practically a function, it’s so elegant.

”But why, Kyle? This seems like overkill!”

Maybe it is. But let’s say I had to add a third Theme: high-contrast. What does our ternary look like then?

const toggleTheme = () => {
  setTheme(currentTheme =>
    currentTheme === 'light'
      ? 'dark'
      : currentTheme === 'dark'
        ? 'high-contrast'
        : 'light',
  )
}

I mean, to be fair, we could probably ditch the ternary and make it a bit nicer:

const toggleTheme = () => {
  setTheme(currentTheme => {
    if (currentTheme === 'light') return 'dark'
    if (currentTheme === 'dark') return 'high-contrast'
    return 'light'
  })
}

But why go through all of that, when we could just update the map?

const NEXT_THEME: Record<Theme, Theme> = {
  dark: 'high-contrast',
  'high-contrast': 'light',
  light: 'dark',
}

Our toggleTheme function doesn’t have to change at all. NEXT_THEME[currentTheme] just keeps working. Bonus points: we didn’t change the cyclomatic complexity of our code at all either.

If we learn to pay attention, we can start to recognize situations where using an explicit map might be a simpler way for us to write and maintain our code.

Key takeaways

  • Consider when enumerating states might serve you better than a boolean
  • Consider using explicit maps to reduce complexity and improve maintainability

”Why didn’t you use new Map()?”

Just to address anyone who might ask this: Yes, we could use new Map() to create “real” maps. However, I find the most compelling reason to use a Map is if you need to use something other than a string for a keys. Given that we were using strings for keys, I think its more appropriate and simple to just use an object literal.