Ryan Killeen

Draft: The Power Of Conditional Typing

Published

You've found an in-draft article! Feel free to look around. Expect to find in-progress articles, missing examples, and little animated construction gifs from 1994.

---

What are Conditional Types?

A lot of code is spent making decisions! Whether it's a function making choices based on input types, or returning different values in different scenarios, our code isn't very useful without conditions.

Typescript has the concept of conditional types to make writing this logic a little easier. The docs do a great job of breaking down many usecases.

A little breaducation

First, a very contrived example. Imagine you have a React component that takes a few properties, some of which are mutually exclusive. In our case, we're re-creating Panera's You Pick Two menu.

type Soup = 'Chicken Noodle' | 'Baked Potato'
type Sandwhich = "Bacon Turkey Bravo" | "Smokehouse BBQ"
type Salad = 'Fuji Apple' | "Caeser"

type Choice1 = {
    choice1: Soup;
    choice2: Sandwhich | Salad;
}

type Choice2 = {
    choice1: Sandwhich;
    choice2: Soup | Salad;
}

type Choice3 = {
    choice1: Salad;
    choice2: Soup | Salad;
}

type Props = Choice1 | Choice2 | Choice3

const example : Props = {
    choice1: "Bacon Turkey Bravo",
    choice2: "Smokehouse BBQ"
}

If you ran this code (TS Playground reference) you'd notice there's an error! If choice 1 is a sandwich, choice 2 can't be a sandwich. That's too many sandwiches.

That's the power of conditional typing, we're able to enforce this logic without writing a single if statement in our Typescript!

Which Select to Select

Now a more pragmatic example, that led me to writing this post. Component Libraries are a great source of complexity: abstracting other libraries, applying additional styles and props, all while trying to make it easier to use.

In our library, we want to provide a <Select /> component. It's mostly a styled version of react-select. Simple enough, right?

import Select from 'react-select'
import { ComponentProps } from 'react'

const customStyles = {
  // ...
}

type SelectProps = ComponentProps<typeof Select>

const StyledSelect = (props: SelectProps) => (
  <Select styles={customStyles} {...props} />
)

export default StyledSelect

Great! But react-select doesn't export just one type of Select. In fact, it exports four distinct flavors:

Each special variant expects unique props in addition to the generally acceptable props, and there's some overlap between them. The Select is either default, async, creatable, or both async and creatable. This could get complex quickly, and there's a few ways to solve it, from a usage and typing perspective:

1. Component Props

We could let users pass the special variants they want to use as a prop:

import Select from '/components/select'
import Creatable from 'react-select/creatable'
import Async from 'react-select/async'

const InstanceOne = () => <Select component={Async} />
const InstanceTwo = () => <Select component={Creatable} />

Totally valid approach! While we can use the prop value to extend types, it puts a lot of work on the user for each special case. Useful for open-ended usage, but in our case it's pretty concretely 4 possibilities.

2. Export four distinct components styled the same

Sometimes brute force is a perfectly acceptable approach (especially when speed is required.) This can be painful to maintain over time and confusing to users on what to use when.

3. Expose props that make sense and let Conditional Types do the work

In this approach, we're choosing to expose two new props: isAsync and isCreatable. The combination of these two props can determine which variant an engineer wants to use.

We'll look at the full example and then break each bit down:


import { ComponentProps } from 'react'
import Select from 'react-select'
import Async from 'react-select/async'
import AsyncCreatableSelect from 'react-select/async-creatable'
import CreatableSelect from 'react-select/creatable'

const Select = ({ isCreatable, isAsync, ...props }) => {

    const SelectVariant =
      isCreatable && isAsync ? AsyncCreatableSelect : 
                 isCreatable ? CreatableSelect :
                     isAsync ? Async : 
                               Select

    return (
      <SelectVariant
        styles={customStyles}
        {...props}
      />
    )
  }
)

type AsyncSelectProps = {
  isAsync: true
} & ComponentProps<typeof Async>

type CreatableSelectProps = {
  isCreatable: true
} & ComponentProps<typeof CreatableSelect>

type AsyncCreatableSelectProps = {
  isCreatable: true
  isAsync: true
} & ComponentProps<typeof AsyncCreatableSelect>

type NormalSelectProps = {
  isAsync: undefined
  isCreatable: undefined
} & ComponentProps<typeof Select>

type SelectTypeProps =
  | AsyncCreatableSelectProps
  | NormalSelectProps
  | CreatableSelectProps
  | AsyncSelectProps

export type SelectProps = {
  isCreatable?: boolean
  isAsync?: boolean
} & SelectTypeProps 

export default Select