Skip to content

Conditional checking of props

Posted on:August 8, 2021 at 10:36 AM

Lets say I have a accordion component. Implementation details are not relevant here 🙂.

export type AccordionPropsType = {
  options: AccordionOptions;
  expanded: boolean;
  collapsed: boolean;
}
const Accordion: FC = () => {
  return ...
}

This accordion has two states it can be either expanded

<Accordion options={options} expanded></Accordion>

or in collapsed state

<Accordion options={options} collapsed></Accordion>

but it can not be in both states.

<Accordion options={options} collapsed expanded></Accordion>

This can be checked at runtime but it will be better experience for the user of the accordion to know it at compile time. Lets see how this can be done via TypeScript.

type ExpandedType = { expanded: boolean, collapsed?: never };
type CollapsedType = { expanded?: never, collapsed: boolean };
export type AccordionPropsType = { options: AccordionOptions; } & (ExpandedType | CollapsedType);

The intresting part of the code is (ExpandedType | CollapsedType). Lets see how this works. The ExpandedType type has two keys one is expanded which can have a boolean value and the other key is collapsed. This is an optional key which has a type of never so this means it’s value can be omitted but it can not be assigned any value. So this means following only following variations are allowed

const expandedObject: ExpandedType = { expanded: true }
const expandedObject: ExpandedType = { expanded: false }

The same thing happens in CollapsedType with keys changed. So following are valid variations for it

const collapsedType: CollapsedType = { collapsed: true }
const collapsedType: CollapsedType = { collapsed: false }

Now lets take union of both these types (ExpandedType | CollapsedType) and see what are valid variations.

const variation1: ExpandedType | CollapsedType = { expanded: true };
const variation2: ExpandedType | CollapsedType = { expanded: false };

These are allowed because this satisfies ExpandedType.

const variation1: ExpandedType | CollapsedType = { collapsed: true };
const variation2: ExpandedType | CollapsedType = { collapsed: false };

These are also allowed because they satisfies CollapsedType. The following its variation where both collapsed and expanded are not allowed

const variation1: ExpandedType | CollapsedType = { expanded:true, collapsed: true };

const variation2: ExpandedType | CollapsedType = { expanded:true, collapsed: false };
const variation2: ExpandedType | CollapsedType = { expanded:false, collapsed: true };
const variation2: ExpandedType | CollapsedType = { expanded:false, collapsed: false };

Lets see why the variation1 fails. variation1 can either of ExpandedType or CollapsedType. So when TypeScript reads the line { expanded:true, collapsed: true }; it first tries to if it is possible to represent this object with the ExpandedType. The ExpandedType has a key called expanded which does infact takes a key expanded with value boolean so far good. Next TypeScript sees variation1 has another property called collapsed. TypeScript finds this key in ExpandedType but value for key collapsed in ExpandedType is never which means the collapsed key can not be assigned any value so the checks fails. Same procedure is applied for CollapsedType of variation1 and it also fails for the similar reason.

A similar procedured happens for the other three variations too. Please note that this process is simplified for understanding and the acutal implementation of these checks might differ but the concept is same.

While searching on this topic I came to know that this feature of TypeScript is called Discriminated Union, and there is a excellent article which explains this in more details.