October 30, 2024

Prefer Explicit Maps

edit

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.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Need help with your software problems?

My team and I are ready to help you. Hire Agathist to build your next great project or to improve one of your existing ones.

Get in touch
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Agathist
Good software by good people.
Visit https://agath.ist to learn more
Logo for Array.reduce()
Array.reduce()
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.