ADT components

I don't like Javascript and don't think most sites need to be interactive single page applications. With that in mind I've endeavored to keep this place cosy by shielding all three of my readers from any mention of JS/TS/React/etc. The market disagrees with me of course, which is why I too write frontend code with Typescript and React in my day job. It could be way worse, but it could also be better. That's not just an idle complaint; TS and React, as they exist today, can be coaxed into something quite clean and powerful, provided you're willing to transcend existing conventions.

Some notes on type-driven React components follow. Apologies for making such a "corporate" entry, but I'm eager to share anything that lets me be lazier and offload my worries onto the compiler. This approach may not apply to all use cases, but I think it handles the common "JSON response → render mostly static component" pipeline well. I guess the implementation could use some fine tuning though. There are a lot of ways to write a tagged union, some less complex than the one printed below. I went with a model that would abstract certain details away from end users.

Contrived example

Imagine you're a Pet Shop Boy and you've got certain wares you're trying to part with. For whatever reason, magpie food and iguana leashes aren't selling themselves, so you write a little <Deals> component to sit on the front page and nudge customers into buying some. The iguana leashes come in three different lengths, and you'd like to display them all on a separate line from the product description. Being the savvy marketer that you are, you're also doing A/B testing based on the last digits of customers' Social Security Numbers, and offering 50% off to everybody with an even digits to see if that spurs purchases. So <Deals> also needs to let somebody know if they qualify for a discount.

Mockups

There are five separate states for <Deals>.

Magpie feed

Hot deal: Warbling Acres Magpie Feed

Every maggie in the neighborhood will swoop right into your yard once you spread the good stuff.

[Buy now]

Magpie feed: 50% off

Hot deal: Warbling Acres Magpie Feed <50% off!>

Every maggie in the neighborhood will swoop right into your yard once you spread the good stuff.

[Buy now]

Iguana leashes

Hot deal: Gondwana Iguana Leashes

Traverse the earth with your green guy in tow.

Available in 3 ft, 5ft, 15ft.

[Buy now]

Iguana leashes: 50% off

Hot deal: Gondwana Iguana Leashes <50% off!>

Traverse the earth with your green guy in tow.

Available in 3 ft, 5ft, 15ft.

[Buy now]

Error

We're sorry, but we're unable to fetch any hot deals at the moment.

Server response

A lot of frontend complexity can be reduced by having a backend schema that renders whatever it can ahead of time, but assuming we not working with something so nice, consider the following "maximum" and "minimum" sets of props that <Deals> can work with.

Iguana leashes: 50% off

{
  "title": "Gondwana Iguana Leashes",
  "dealMessage": "50% off!",
  "description": "Traverse the earth with your green guy in tow.",
  "sizes": [3, 5, 15],
  "link": "https://westendpetshop.com/products?id=1917"
}

Error

{
  "title": null,
  "dealMessage": null,
  "description": "We're sorry, but we're unable to fetch any hot deals at the moment.",
  "sizes": null,
  "link": null
}

Response type

With so much variation in data shape, most of the fields in the props for <Deal> need to be nullable.

type DealResponse = {
  title: string | null;
  dealMessage: string | null;
  description: string;
  sizes: number[] | null;
  link: string | null;
};

Rendering

If the DealResponse type is passed directly to <Deal> as its props, all five variations can be rendered with some inline boolean logic.

import React, { FC } from 'react';

const Deals: FC<DealResponse> = ({ title, dealMessage, description, sizes, link }: DealResponse) => (
  <div>
    {title && (
      <div>
        <span>`Hot deal: ${title}`</span>
        {dealMessage && <span>{dealMessage}</span>}
      </div>
    )}
    <p>{description}</p>
    {sizes && (
      <p>`Available in ${sizes.reduce((acc: string, x: number) => acc + `${x}ft, `, '')}`</p>
    )}
    {link && <a href={link}>Buy now</a>}
  </div>
);

Something like that. Not quite, but you get it. Please don't make me deal with JSX any more than I have to. It's my day off.

Interspersing HTML and logic like this is touted as one of React's strengths, but I think it looks like shit. Simple elements quickly become unreadable tangles of curly braces and null checks the moment the props start to vary a bit. This "idiomatic" approach proves even more asinine if any of its nullable props are passed along to child components, and potentially need to be validated all over again. There is a better way to go about all this.

Do repeat yourself

If the designer presents you with n mockups, then I think it's perfectly acceptable to create something approaching n separate components, each with a designated set of props. You can use the type system to iron out a lot of needless logic at compile time, and wind up with a far more maintainable design in the long run. Setting aside dealMessage for the time being, there are three distinct shapes that <Deal> has to handle.

type MagpieProps = {
  title: string;
  description: string;
  link: string;
};

type IguanaProps = {
  title: string;
  description: string;
  sizes: string;
  link: string;
};

type ErrorProps = {
  description: string;
};

null has already been completely removed from the equation here. As you might imagine, any accompanying <MagpieDeal>, <IguanaDeal>, and <ErrorDeal> components that take these types as inputs would be devoid of conditional logic, but the "JSON response → render component" process needs one extra step to facilitate this.

Parse, don't validate

I did not coin this phrase, but I will gladly parrot it whenever I can. If your input data varies in some manner, it makes far more sense to encode this variation once at the type level, rather than damning oneself to check against deviance over and over again in the runtime code. With this advice in mind, the flow of <Deal> should look more like the following.

  1. Fetch DealResponse object.
  2. Run a function that parses it into MagpieProps, IguanaProps, or ErrorProps. Any non-string fields, such as sizes, should be coalesced into string form during parsing, since there is no need to perform numerical operations on them. Keep the data as dumb as possible.
  3. Have <Deal> receive the DealProps, which is a union of these three props types.
  4. <Deal> renders <MagpieDeal>, <IguanaDeal>, or <ErrorDeal> based upon the union instance it receives.

Implementing this pattern isn't too difficult, and really makes the whole experience of writing Typescript less typeshit.

Tagged unions

In Haskell, it's easy enough to represent DealProps as an algebraic data type

data DealProps = MagpieProps | IguanaProps | ErrorProps

and match against it accordingly. This same ADT concept can be implemented in Typescript with just as much type safety and only slightly uglier syntax. Right off the bat, you can write a union type like

type DealProps = MagpieProps | IguanaProps | ErrorProps;

but it will differ from Haskell in a crucial way: there is nothing to pattern match against.

Typescript is capable of using string literals as types, however. Even though they overload JS's string syntax, they are not strings. Just like Just a | Nothing in Haskell, unions of these string literals are closed sets that can be exhaustively matched against, with compile time feedback. Consider the same union, with these literal tags.

type DealProps =
  | { tag: 'Magpie'; value: MagpieProps }
  | { tag: 'Iguana'; value: IguanaProps }
  | { tag: 'Error';  value: ErrorProps };

When provided a DealProps type now, <Deal> can pattern match against the common tag field and delegate a shape to its respective component. Since tag is not a string, the compiler will warn the programmer for attempting to match against something outside the set, like MagpeeResponse, or missing a case entirely.

Since this pair of tag and value is something common to all union instances, it can be abstracted a bit, and given a dedicated tag constructor that avoids having to declare explicit object fields.

type Tagged<A, B> = { tag: A; value: B };

const tag = <A, B>(label: A, vals: B): Tagged<A, B> => ({ tag: label, value: vals });

type DealProps =
  | Tagged<'Magpie', MagpieProps>
  | Tagged<'Iguana', IguanaProps>
  | Tagged<'Error',  ErrorProps>;

Parsing

We'll get to the pattern matching soon enough, but a DealResponse → DealProps transformation function is required first. This vanilla function will perform all the null checks previously handled in the TSX itself, and ensure the actual components have nothing but their expected states to handle.

const parseResponse = ({ title, description, sizes, link }: DealResponse): DealProps => {
  const parseSizes = (xs: number[]): string => xs.reduce((acc: string, x: number) => acc + `${x}, `, '');
  const parseTitle = (x: string): string => `Hot deal: ${x}`;
  return title && link
    ? sizes
      ? tag('Iguana', { title: parseTitle(title), description, sizes: parseSizes(sizes), link })
      : tag('Magpie', { title: parseTitle(title), description, link })
    : tag('Error', { description });
};

TS's awareness of the DealProps type is just as precise as Haskell would be. This function will not compile if it returns tag('Magpie', {… sizes …}), since it knows that only the IguanaProps instance of its return type can possess such a field.

Dispatching

I will not explore the implementations of <MagpieDeal>, <IguanaDeal>, or <ErrorDeal> yet, because the rendering of dealMessage needs to be explored separately, but dispatching these transformed types is almost as straightforward as Haskell pattern matching.

const Deal: FC<DealResponse> = (response: DealResponse) => {
  const props = parseResponse(response);
  return (
    props.tag === 'Magpie' ? MagpieDeal(props.value) : 
    props.tag === 'Iguana' ? IguanaDeal(props.value) : 
    /* otherwise */          ErrorDeal(props.value)
  );
};

While writing this function, the TS compiler can provide autocomplete feedback for yet-to-be matched tags, and error out if props.value is an ambiguous shape.

Optional fields

The general concept of a strongly typed tagged union can be built upon to provide common abstractions like Maybe or Either. This article won't explore what functor/monad instances look like for such types, or else I might have written them differently, but such things are possible in Typescript as well. In any case, Maybe is used to represent data that simply is or isn't available.

data Maybe a = Just a | Nothing

This might be represented using the aforementioned Tagged type, along with constructors, like so.

type Maybe<A> = Tagged<'Just', A> | Tagged<'Nothing', null>;

const nothing = (): Tagged<'Nothing', null> => tag('Nothing', null);
const just = <A>(value: A): Tagged<'Just', A> => tag('Just', value);
const maybe = <A>(value: A | null): Maybe<A> => (value ? just(value) : nothing());

Running maybe() against a nullable value will tag it accordingly. The true beauty of this encoding reveals itself through functors, applicatives, and monads, but it can also be used to render things cleanly. Recall the following TSX line.

{dealMessage && <span>{dealMessage}</span>}

We want to render this data if it exists, but do nothing otherwise. This is a common pattern in components that can be abstracted across optional props.

const Maybe = <A>(component: FC<A>, args: Maybe<A>): ReactElement | null =>
  args.tag === 'Just' ? component(args.value) : args.value;

Returning ReactElement | null means "render or do nothing" in React parlance. Assuming there were some styled <DealMessage> component, and dealMessage were now typed as Maybe<string>, the TSX could be written as follows.

<Maybe component={DealMessage} args={dealMessage} />

Judicious use of Maybe can reduce the need to declare too many separate sub-components. It depends on what the actual layout is meant to look like, but MagpieDeal | IguanaDeal could arguably be simplified into a single component with a sizes: Maybe<string> field.

The full picture

type Tagged<A, B> = { tag: A; value: B };
const tag = <A, B>(label: A, vals: B): Tagged<A, B> => ({ tag: label, value: vals });

type Maybe<A> = Tagged<'Just', A> | Tagged<'Nothing', null>;
const nothing = (): Tagged<'Nothing', null> => tag('Nothing', null);
const just = <A>(value: A): Tagged<'Just', A> => tag('Just', value);
const maybe = <A>(value: A | null): Maybe<A> => (value ? just(value) : nothing());

type DealResponse = {
  title: string | null;
  dealMessage: string | null;
  description: string;
  sizes: number[] | null;
  link: string | null;
};

type MagpieProps = {
  title: string;
  dealMessage: Maybe<string>;
  description: string;
  link: string;
};

type IguanaProps = {
  title: string;
  dealMessage: Maybe<string>;
  description: string;
  sizes: string;
  link: string;
};

type ErrorProps = {
  description: string;
};

type DealProps =
  | Tagged<'Magpie', MagpieProps>
  | Tagged<'Iguana', IguanaProps>
  | Tagged<'Error', ErrorProps>;

const parseResponse = ({title, dealMessage, description, sizes, link }: DealResponse): DealProps => {
  const parseSizes = (xs: number[]): string =>
    'Available in sizes ' + xs.reduce((acc: string, x: number) => acc + `${x}, `, '');
  const parseTitle = (x: string): string => `Hot deal: ${x}`;
  return title && link
    ? sizes
      ? tag('Iguana', {
          title: parseTitle(title),
          dealMessage: maybe(dealMessage),
          description,
          sizes: parseSizes(sizes),
          link,
        })
      : tag('Magpie', {
          title: parseTitle(title),
          dealMessage: maybe(dealMessage),
          description,
          link,
        })
    : tag('Error', { description });
};

const Maybe = <A>(component: FC<A>, args: Maybe<A>): ReactElement | null =>
  args.tag === 'Just' ? component(args.value) : null;

const MagpieDeal: FC<MagpieProps> = ({ title, dealMessage, description, link }: MagpieProps) => (
  <div>
    <div>
      <span>{title}</span>
      <Maybe component={DealMessage} args={dealMessage} />
    </div>
    <p>{description}</p>
    <a href={link}>Buy now</a>
  </div>
);

const IguanaDeal: FC<IguanaProps> = ({title, dealMessage, description, sizes, link}: IguanaProps) => (
  <div>
    <div>
      <span>{title}</span>
      <Maybe component={DealMessage} args={dealMessage} />
    </div>
    <p>{description}</p>
    <p>{sizes}</p>
    <a href={link}>Buy now</a>
  </div>
);

const ErrorDeal: FC<ErrorProps> = ({ description }: ErrorProps) => (
  <div>
    <p>{description}</p>
  </div>
);

const Deal: FC<DealResponse> = (dealResponse: DealResponse) => {
  const props = parseResponse(dealResponse);
  return (
    props.tag === 'Magpie' ? MagpieDeal(props.value) :
    props.tag === 'Iguana' ? IguanaDeal(props.value) :
                             ErrorDeal(props.value)
  );
}

Conclusion

It's a mouthful, but it's completely unambiguous and incredibly easy to maintain. Designer mocks up a new state? Product people demand another A/B test? Just make a new union instance and sub-component. I don't expect everybody—even most JS programmers—to like it right away, but they can be convinced otherwise. Some of my colleagues are not keen on this design and think unions are some kind of theoretical wank from FP land, but they are stuck using them regardless of which approach they take. It's a question of having a single tagged parent level union versus multiple anonymous sibling level ones like string | null.

It's also worth keeping in mind that a lot of boilerplate can be reduced through further abstraction. Like I said before, the difference between <MagpieDeal> and <IguanaDeal> is better represented with Maybe, but I wanted to illustrate a three instance union. Implementing an Either type with associated rendering behavior might have simplified this even further, since <ErrorDeal> could be something generic throughout the site.

Another thought: it's possible for a parser to return components directly, but if these components have children components, it's better to keep things off as tagged props in my opinion.

At the risk of sounding arrogant, I believe a refined version of this type-driven rendering will become commonplace in the future of frontend development. The concept just hasn't made its rounds among JS programmers, who are approaching all this type system business gingerly for the time being. If the syntax were simplified and advocates can avoid using scary words like "algebraic data type," I can see something like this catching on. The benefits are self evident.