Draft: The Power Of Conditional Typing
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:
- Default
- Creatable
- Async
- AsynCreatable
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