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.