❌ How to use TypeScript to make undesirable UX states impossible (#73)

November 3, 2022

Happy Thursday!

I hope you are doing well and enjoying the fall weather wherever you are in the world! I know that I certainly am. I'm extra happy because I rescued a puppy a couple of weeks ago and he is way too cute. Respond to this email if you want to see pictures of him...ok back to work!


Lately, I've been thinking a lot about how to architect a component library to drive design consistency in the product. I often use TypeScript to create component interfaces that make undesirable UX states impossible.

We'll use a dropdown component with combobox support (aka a searchbar at the top that lets you filter for your desired properties) as an example.

Imagine your design system contains a dropdown component. It's interface looks something like this:

interface PanelItem { itemType: "button" | "anchor"; label: string; } interface DropdownProps { panelItems: PanelItem[]; } <Dropdown panelItems={[ { itemType: "button", label: "First item" }, { itemType: "anchor", label: "Second item" }, { itemType: "button", label: "Third item" }, ]} />;

As a developer on the team, you're tasked with adding combobox support to this component.

You decide that you'll support this by allowing consumers to add an additional panel item that looks like this: {itemType: 'search'}.

You go ahead and make that change, merge your PR, and ship the new feature.

interface PanelItem { + itemType: "button" | "anchor" | "search"; + label?: string; - itemType: "button" | "anchor"; - label: string; } interface DropdownProps { panelItems: PanelItem[]; } <Dropdown panelItems={[ + { itemType: "search" }, { itemType: "button", label: "First item" }, { itemType: "button", label: "Second item" }, { itemType: "button", label: "Third item" }, ]} />

This seems to work well, until you're looking at your product and you notice that someone has used your component, and put the search bar in the middle of the dropdown panel. That doesn't match the design system!

They've done something that looks like this:

<Dropdown panelItems={[ { itemType: "button", label: "First item" }, { itemType: "search" }, { itemType: "button", label: "Second item" }, { itemType: "button", label: "Third item" }, ]} />

You realize that the interface you designed unintentionally allows the component to end up in a state where the UX doesn't match the design system!

We can use TypeScript to avoid this case by enforcing that the first item in the panelItems array can only be a search bar, and nothing else. By making the following changes, we can now prevent someone from (accidentally or intentionally) introducing design inconsistency in our product.

interface PanelItems { + itemType: 'button' | 'anchor' - itemType: 'button' | 'anchor' | 'search'; label?: string; } interface DropdownProps { + panelItems: [{ itemType: "search" }, ...PanelItem[]] | PanelItem[]; - panelItems: DropdownPanel[]; }

Now, if we try to put a search bar in the middle of the dropdown panel, we'll get a TypeScript error indicating that something is wrong.


Talk soon,

Mae

Do you want design systems tips and tricks sent to your inbox?