Avatar

Bonjour, I'm Julia.

Deriving a Typescript Union Type From an Array or Object

#react #typescript

3 min read

I've been working with a number of datasets recently, within my React app. They normally take the form of an array of values, or an object with key-value pairs, where the values in both the array and object tend to be objects themselves. So for example:

// Array of objects

export const FAV_MOVIES = [
  {
    name: 'Fight Club',
    genre: 'action',
    bookAuthor: 'Chuck Palahniuk',
    summary: "It's the same person!",
  },
  {
    name: 'Harry Potter',
    genre: 'fantasy',
    bookAuthor: 'JK Rowling',
    summary: 'Love conquers all',
  },
  {
    name: 'The Godfather',
    genre: 'crime',
    bookAuthor: 'Mario Puzo',
    summary: "You can't get away from the family business",
  },
]
// Object of objects

export const MOVIES = {
  'Fight Club': {
    imageName: 'fightclub',
    genre: 'action',
    bookAuthor: 'Chuck Palahniuk',
  },
  'Harry Potter': {
    imageName: 'harrypotter',
    genre: 'fantasy',
    bookAuthor: 'JK Rowling',
  },
  'The GodFather': {
    imageName: 'thegodfather',
    genre: 'crime',
    bookAuthor: 'Mario Puzo',
  },
  'The Hobbit': {
    imageName: 'thehobbit',
    genre: 'fantasy',
    bookAuthor: 'JRR Tolkien',
  },
  'Ready Player One': {
    imageName: 'readyplayerone',
    genre: 'sci-fi',
    bookAuthor: 'Ernest Cline',
  },
}

As I'm using Typescript, I was trying to work out how you go about deriving union types from the values of certain keys from these objects. To be sure, the simplest thing to do would've been to just declare these values as a string, say if I wanted to pass these values into a MoviesComponent.

interface MoviesComponentProps {
  movieName: string
  movieGenres: string[]
}

This doesn't utilise the best that Typescript offers however, so I wanted to impose stricter typing by limiting what the values of these strings could be. As of Typescript 3.4, there's a fairly easy way of doing this, with the as const syntax. So taking the MOVIES object, we can add as const after the closing brace, which locks MOVIES to becoming a read-only constant.

export const MOVIES = {
  'Fight Club': { imageName: 'fightclub', genre: 'action', bookAuthor: 'Chuck Palahniuk' },
  'Harry Potter': { imageName: 'harrypotter', genre: 'fantasy', bookAuthor: 'JK Rowling' },
  'The GodFather': { imageName: 'thegodfather', genre: 'crime', bookAuthor: 'Mario Puzo' },
  'The Hobbit': { imageName: 'thehobbit', genre: 'fantasy', bookAuthor: 'JRR Tolkien' },
  'Ready Player One': { imageName: 'readyplayerone', genre: 'sci-fi', bookAuthor: 'Ernest Cline' },
} as const

This allows us to create a couple of new types, firstly to define the keys (MovieName), which we can then use to access each of the values (i.e. the objects). So for the movie genre...

type MovieName = keyof typeof MOVIES // 'Fight Club', 'Harry Potter', ...
type MovieGenre = typeof MOVIES[MovieName]['genre'] // 'action', 'fantasy', ...

...which we can add to our MoviesComponentProps for stricter typing.

interface MoviesComponentProps {
  movieName: MovieName
  movieGenres: MovieGenre[]
}

So what about if we're dealing with an array of objects? We can do something similar as well, through the as const syntax again.

export const FAV_MOVIES = [
  {
    name: 'Fight Club',
    genre: 'action',
    bookAuthor: 'Chuck Palahniuk',
    summary: "It's the same person!",
  },
  {
    name: 'Harry Potter',
    genre: 'fantasy',
    bookAuthor: 'JK Rowling',
    summary: "Love conquers all",
  },
	{
    name: 'The Godfather',
    genre: 'crime',
    bookAuthor: 'Mario Puzo',
    summary: "You can't get away from the family business",
  },
] as const

To create a type for the book authors, we can access each item in the array with [number]:

export type BookAuthor = typeof FAV_MOVIES[number]['bookAuthor']; // 'Chuck Palahniuk', ...

And that's it! It keeps everything strongly typed and has the nice bonus of IDE auto-completion when passing these values around the app.

© 2016-2024 Julia Tan · Powered by Next JS.