Typescript type shenanigans: Conditional Types

10 Jun 2018 in programming | typescript | react |

The amazing thing about Typescript’s type system is not really that it strives to be the most complete type system, or the most formal, but in that in its pursue to be able to type all the things that lovers & haters of javascript do with the language, it explores new avenues and possibilities. This post looks at a use case for a fairly recent addition, conditional types.

But before we get there, bask in the glory of this nifty type (I saw it first in the typing for redux.

export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };

What does it do? - Given some type, it recursively turns itself and all of its properties as optional (remember that the in-built Partial<T> does it flat on T).

You don´t have to remember that. It has no relevance to what follows. It’s just a cool and useful type that shows off all the parts that Typescript has learned in the past year or so.


Right. Conditional Types. To me it always helps to have some example. This one also involves some react. Sorry.

Imagine some UI component which you can use in either of two ways.

  • I give it a list of items which contain links and the component renders the links in some useful and pretty fashion
  • I give it a list of items which contain actions (functions to be invoked when some button is clicked), and now the component is to render buttons with the click triggering those actions.

Note that the two item types are mutually exclusive. I either want it to render links, or buttons. Ideally I want Typescript to guide me so that I’m filling in the details correctly.

Let’s encode the two types of functionality:

type ActionableItemType = "action" | "link";

and the two kinds of items that I want to support:

export interface ActionItem {
  label: string;
  action: () => any;
}

export interface LinkItem {
  label: string;
  link: string;
}

Now we are already ready to define our conditional type 🎉 !

type ActionableItem<T extends ActionableItemType> = 
  T extends "action" 
  ? ActionItem
  : T extends "link" 
  ? LinkItem 
  : never;

what it says is that given some Type T, when T extends the type “action”, ActionableItem is now an ActionItem, otherwise if T is “link”, ActionableItem is LinkItem. To complete the type, we use the never-Type, which is exactly for such cases where you want to use a Type that no instance can have.

We are now armed to implement a react component that takes advantage of this typing, starting with defining the properties of the component.

export interface ActionableItemComponentProps<T extends ActionableItemType> {
  items: Array<ActionableItem<T>>;
}

And the component itself

class ActionableItemComponent<T extends ActionableItemType> extends React.Component<
  ActionableItemComponentProps<T>
> {
    // ... impl to come
}

The component itself is now generic, that is when we work with the items passed in we don’t know if these are action or link items (remember the runtime is javascript, so all your type info is pretty much erased at runtime). To get back typings you will make use of type guards, a runtime check that gives your code safe areas where you may assume that something is indeed of the type you expect. Here are the two type guards that I will use in the render-code

function isActionsArray(items: any[]): items is ActionItem[] {
  return items.length > 0 && items[0].action;
}

function isItemsArray(items: any[]): items is LinkItem[] {
  return items.length > 0 && items[0].link;
}

You can see that I only check the first item of some array to check whether I have action or link types. The underlying assumption is that the call site is also written in TypeScript. You will see that the compile time check will ensure that all items must adhere to the same type.

Now we are ready to implement a render function for the ActionableItemComponent.

render() {
  const { items } = this.props;
  return (
    <Container>
      {(isActionsArray(items) && this.renderActionItems(items)) ||
        (isItemsArray(items) && this.renderLinkItems(items))}
    </Container>
  );
}

The implementation of renderActionItems and renderLinkItems is left to the reader (No, it’s not, you can actually find it here).

The only thing left is to make the component readily available to users of the component (The generic in there means we can’t use it easily straight away), so we provide two convenience types to have the two different styles of rendering actionable items:

type ActionItemsComponent = new () => ActionableItemComponent<"action">;
export const ActionItemsComponent = ActionableItemComponent as ActionItemsComponent;

type LinkItemsComponent = new () => ActionableItemComponent<"link">;
export const LinkItemsComponent = ActionableItemComponent as LinkItemsComponent;

Users of the component can now go ahead and use it as such:

import * as React from "react";
import { ActionItemsComponent, LinkItemsComponent } from "./ActionableItems";

export const UsingActions : React.SFC = () => (
    <ActionItemsComponent items={[{action: () => alert("Hallelujah"), label: "alerted" }]} />
);

export const UsingLinks : React.SFC = () => (
    <LinkItemsComponent items={[{link: "/#whatever", label: "whatever link" }]} />
);

Depending on either usage, the consumer gets nice type safety on how the items need to be structured, while the implementation can make use of the conditional type to provide a single implementation.

Of course there are many ways to encode such functionality, and if the two styles of rendering diverge a lot, you may be better off implementing two different components. Even so, I hope it helps you to consider in what ways conditional types can be useful to you.

Chronology

  |  
comments powered by Disqus