loading...
Cover image for Using TypeScript ADT's to write more reliable React

Using TypeScript ADT's to write more reliable React

rametta profile image Jason ・4 min read

You may have heard of Algebraic Data Types (ADT's) before but didn't understand how they can be applied to everyday code - so this article will provide some examples and explanations of why you should start using them.

Before we get into ADT's, let's go over the foundation of what ADT's are made of.

Basic Types

In Javascript, you can not declare a type that prevents other types from being assigned to it. The below example we see that anything can be assigned to the language variable. We can assign a number or a boolean or an object to it if we later wanted, but that may cause bugs in the future if we were not expecting the variable type to be something other than a string.

let language = 'en'

In Typescript, we get more control over declaring types. See below, now we can only assign a string to the language variable, which is much better. Now when we access this variable in the future, we are pretty certain that the value will be a string and proceed accordingly.

let language: string = 'en'

But we can do better...

Union Types

With Typescript Union Types, we can say something can be more than one type. 😮 In the below example, we see that the language variable can either be a string OR a number.

let language: string | number = 'en'

You might be saying to yourself, "cool, but why would I want two different types in one variable?"

This is a great question, but before we figure out why we would need this, we need to understand that anything can be considered a type in Typescript, including specific string values.

So now we can specify exactly which values can be assigned to the language variable.

let language: 'en' | 'fr' | 'ru' = 'en'

Now we can only assign certain values to language.

The interesting question now is, "how do we know which type is currently stored?"

If we have a variable that can hold two different types, when we access that value, we must check to see what the type is before we do something with it.

let nameOrID: string | number = 'Jason'

if (typeof nameOrID === 'string') {
  // do something with string...
} else if (typeof nameOrID === 'number') {
  // do something with number...
}

This is important, because if we do not check the value type then we do not know which one of the types is currently being used - so we might try to do math with a string, or do a string operation on a number...

Knowing these things, now we can explain Typescript's Discriminated Union Type.

Discriminated Union Types

Using the things we learned about Union Types, we can construct a special union that obeys some rules.

The rules that should be followed are:

  1. All types in the union, share a common property.
  2. There needs to be a union type declared from the types.
  3. There must be type guards on the common property.

Here is an example:

type HockeyGame = {
  kind: 'hockey' // Rule 1 - common property 'kind'
  homeScore: number
  awayScore: number
  clock: number
  isDone: boolean
}

type BaseballGame = {
  kind: 'baseball' // Rule 1 - common property 'kind'
  inning: number
  isTop: boolean
  stadium: string
}

// Rule 2 - Union type declared
type Game = HockeyGame | BaseballGame

const gameToString = (game: Game): string => {
  // Rule 3 - Type guard on the common property
  switch (game.kind) {
    case 'hockey':
      return `Hockey game clock: ${game.clock.toString()}`
    case 'baseball':
      const frame = game.isTop ? 'top' : 'bottom'
      return `Baseball game is in the ${frame} of inning ${game.inning}`
  }
}

In the example above, we see that we used what we learned about assigning specific strings to a type with the kind property. The kind property can only ever be hockey or baseball and never anything else.

This common property acts as an ID for the object and allows us to know what other properties are defined and available to be accessed.

Following these rules will allow the Typescript compiler to know which fields are available. So if you have checked the guard and deemed it to be hockey then the compiler will only allow you to access the fields from the HockeyGame type.

This can prevent a lot undefined errors you may get from accessing properties that may or may not be there at different times.

ADT's with React

Now let's see how we can take advantage of this pattern in React.

Using the Game types declared above, we can safely render different components based on the common property in the union.

const HockeyGameBox = ({ game }: { game: HockeyGame }) => (
  <div>
    {game.homeScore} - {game.awayScore}
  </div>
)

const BaseballGameBox = ({ game }: { game: BaseballGame }) => (
  <div>
    {game.inning} - {game.stadium}
  </div>
)

const renderGame = (game: Game) => {
  switch (game.kind) {
    case 'hockey':
      return <HockeyGameBox game={game} />
    case 'baseball':
      return <BaseballGameBox game={game} />
  }
}

const GamePage = () => {
  const [games] = useState<Game[]>([
    /* mix of different games */
  ])
  return games.map(renderGame)
}

As you can see, using ADT's can greatly reduce the amount of runtime bugs you get when using dynamic data. It's not a silver bullet for preventing bugs, but it's a step in the right direction.

To learn more about ADT's, check out my post about it in Elm: Elm's Remote Data Type in Javascript

Posted on by:

rametta profile

Jason

@rametta

Software Developer in Montreal, Canada.

Discussion

markdown guide
 

This is a really elegant way of cleaning up components that are shared between types!
Thanks!