loading...

Typescript - Button Color State with ADTs

piq9117 profile image Ken Aguilar ・5 min read

I've seen enough typescript code that I can say that booleans are over used. We need to normalize the use of sum types and reserve the use of booleans.

There are not a lot of things that booleans can represent. Even something binary like the state of a light bulb can get confusing when it's represented with a boolean. Should True represent the light bulb being off or on, which one should False represent? I'm sure with lots of comments it will be fine, but as we all know comments are not too effective in conveying information to other contributors.

const toggleLightBulbState = ( lightBulbState: boolean ): boolean => {
  // if the lightbulb is on return false to turn it off.
  if ( lightBulb == true ) {
    return false;
  } 
  // otherwise turn on the light bulb.
  else {
    return true;
  }
}

Imagine this withtout comments, would you able able to understand what this code is doing?

Let's go through a contrived example and deal with button color states and fake http states, and eventually convert it to use sum types.

We'll be using bulma css classes in the example

import React from "react";

const colorToClassName = ( color: string ): string => {
  if ( props.color === "blue" ) {
    return "is-info";
  } 

  else if ( props.color === "red" ) {
    return "is-danger";
  }

  else {
    return "is-warning";
  }
}

export function Button(props: { color: string }) => {
  return  <button className={ colorToClassname(color) }>Hello</button>
}

The parent component

import { Button } from "./Button.tsx";

const responseToColor = ( response: string ) => string {
  if ( response === "successs" ) {
    return "blue";
  }
  else if ( response === "warning" ) {
    return "yellow";
  }
  else {
    return "red";
  }
}

export function App() {
  let httpResponse = "error";
  return (
    <div className="container">
      <div className="section">
        <Button color={responseToColor(httpResponse)} />
      </div>
    </div>
  );
}

Equality checks with strings is definitely error prone but it looks like this is fine, especially for something small scale but imagine an actual application with so many more cases and so much more logic. Depending on booleans and string equality will be a source of great agony. What can help us here is the type checker! Even though typescript is not as strong as something like purescript or haskell it can still help a great deal.

With purescript we can do something like this

-- sum type representing all possible colors this button can render
data ButtonColor
  = Blue
  | Red
  | Yellow

-- transforms ButtonColor to Bulma CSS classes.
-- Pattern match the colors and return the matching css class.
-- These bulma classes are not exposed outside this function.
colorToClassName :: ButtonColor -> String
colorToClassName = case _ of
  Blue -> "is-info"
  Red -> "is-danger"
  Yellow -> "is-warning"

The ButtonColor type that represents all the possible colors the button can render can then be exported so it can be used in the colorToClassName function. With a fake http response, we can model it with a sum type and do a case match and transform that into our button colors.

-- type representing http response status.
data Response
  = Success
  | Error
  | Warning

-- transforms http response to button color.
responseToColor :: Response -> ButtonColor
responseToColor = case _ of
  Success -> Blue
  Error -> Red
  Warning -> Yellow

By using types, the purescript type checker can help us verify our program. I'm using "verify" loosely here, I don't mean formal verification.

After that long tangent let's go back to typescript and see if we can recreate what we did in purescript, and let's do it in react! Let's start with the button component and model the possible color states of this button. For a detailed explanation on sum types in typescript, check out Guilio Canti'sarticle. In typesript, sum types are called discriminated unions. In order to determine which type is being matched in a pattern match it needs a discriminant. We'll be using the tag field as our discriminant. This is used in our "poor man's" pattern matching, which is a switch statement.

import React from "react";

interface Red {
  tag: "red";
}

interface Blue {
  tag: "blue";
}

interface Yellow {
  tag: "yellow";
}

export type ButtonColor
  = Red
  | Blue
  | Yellow

// poor man's pattern matching.
const toClassName = ( b: ButtonColor ): string => {
  switch ( b.tag ) {
    case "blue":
      return "is-info";

    case "red":
      return "is-danger";

    case "yellow":
      return "is-warning";

    default:
      return "";
  };
}

export const blue: Blue = ({ tag: "blue" });
export const red: Red = ({ tag: "red" });
export const yellow: Yellow = ({ tag: "yellow" });

export function Button (props: { color: ButtonColor }) {
  return ( <button className={ "button " + toClassName(props.color) }>Hello</button> );
}

Since I don't like using switch statements, let's modify the code a little bit using the Option type, specifically the utility function fromNullalble and it will come from the library fp-ts. We'll also add the className field to the interfaces.

import React from "react";
import { pipe } from "fp-ts/lib/function";
import { fromNullable, map, getOrElse } from "fp-ts/lib/Option";

interface Red {
  tag: "red";
  className: string;
}

interface Blue {
  tag: "blue";
  className: string;
}

interface  Yellow {
  tag: "yellow";
  className: string;
}

export interface NoColor {
  tag: "noColor";
  className: string;
}

export type ButtonColor
  = Red
  | Blue
  | Yellow
  | NoColor;

// implementation of the colors
export const blue: Blue = ({ tag: "blue", className: "is-info" });
export const red: Red = ({ tag: "red", className: "is-danger" });
export const yellow: Yellow = ({ tag: "yellow", className: "is-warning" });
export const noColor: NoColor = ({ tag: "noColor", className: "" });

interface Colors {
  blue: Blue;
  red: Red;
  yellow: Yellow;
  noColor: NoColor;
}

const colors: Colors = {
  blue,
  red,
  yellow,
  noColor
};

const toClassName = ( b: ButtonColor ): string =>
  // pattern matching
  // This "pattern matching" works because they all have the className field.
  pipe(
    fromNullable(colors[b.tag]),
    map((c: ButtonColor) => c.className),
    getOrElse(() => noColor.className)
  );

export function Button (props: { color: ButtonColor }) {
  return ( <button className={ "button " + toClassName(props.color) }>Hello</button> );
}

fromNullable transforms it into an Option type which is another sum type with None and Some<A> as its inhabitants. If we access a property that is not available in colors it will go to the getOrElse function and return noColor, otherwise it will match a color then access the className property.

Now, let's look at the parent component and model the fake http response into something we can understand or something that makes sense in the context of the app, and transform that fake http response to a button color.

Success, Error, and Warning are the possible states for our fake http response here. They are then used in Response to form a union of all possible states of a fake http response.

import React from 'react';
import './App.css';
import {
  Button, blue, red, yellow, ButtonColor, Blue, Red, Yellow
} from "./Button";
import { pipe } from "fp-ts/lib/function";
import { fromNullable, getOrElse, map } from "fp-ts/lib/Option";

interface Success {
  tag: "success";
  color: Blue;
}

interface Error {
  tag: "error";
  color: Red;
}

interface Warning {
  tag: "warning";
  color: Yellow;
}

type Response
  = Success
  | Error
  | Warning

interface Responses {
  success: Success;
  error: Error;
  warning: Warning;
}

export const success: Success = ({ tag: "success", color: blue });
export const error: Error = ({ tag: "error", color: red });
export const warning: Warning = ({ tag: "warning", color: yellow });

const responses: Responses = {
  success,
  error,
  warning
}

const responseToColor = (res: Response): ButtonColor =>
  // pattern matching
  pipe(
    fromNullable(responses[res.tag]),
    map((r: Response) => r.color),
    getOrElse(() => red as ButtonColor) // have to tell typescript red is a member of ButtonColor
  );

export function App() {
  let fakeHttpResponse = error;
  return (
    <div className="container">
      <div className="section">
        <Button color={responseToColor(fakeHttpResponse)} />
      </div>
    </div>
  );
}

As a result we have eleminated booleans in our tiny app and made the possible states of button colors and fake http response more explicit. We didn't have to figure out whether True would mean "blue", "red", or "yellow". We didn't have to write long comments explaining how booleans would represent our fake http response.

Acknowledgements

Functional design: Algebraic Data Types - Guilio Canti

fp-ts

Example Repo

Button States

Posted on by:

piq9117 profile

Ken Aguilar

@piq9117

* Functional Programmer * All my posts are cross post from my site https://www.taezos.dev/notes.html

Discussion

markdown guide
 

Nice write up. To simplify the type and value definitions you could specify them in one go by leveraging morphic-ts or the fork in matechs-effect. Could be a nice addition to the post as final roll up :)