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
Top comments (1)
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 :)