Sometimes, we need to create a component that serves multiple use-cases. As such, depending on some key attribute, the set of all others might differ. One example is a date picker component. Let’s see how we can implement it.
Imagine our date picker should allow a consumer to select a single date of a range of dates. Let’s try to visualise such use-cases:
<DatePicker
onChange={handleOnChange}
theDate={someDate}
/>
<DatePicker
mode="range"
onStartDateChange={handleStartDateChange}
onEndDateChange={handleEndDateChange}
startDate={someStartDate}
endDate={someEndDate}
/>
We can describe such behaviour in types like this:
type SingleDatePickerProps = {
mode: "single";
onChange: (date: Date) => void;
};
type RangeDatePickerProps = {
mode: "range";
onStartDateChange: (date: Date) => void;
onEndDateChange: (date: Date) => void;
startDate: Date;
endDate: Date;
};
type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
Please, note that here, the mode
attribute is not optional. However, the requirement dictates that it must be optional, and when this attribute omitted, the component must work as a date picker for a single date. Luckily, TypeScript allows us to express this behaviour using Conditional Types:
type UponKey<T, A, B> = T extends undefined ? A : B;
This type expresses the if-then-else
logic. We can use it like this:
function DatePicker(
props: UponKey<
DatePickerProps["mode"],
SingleDatePickerProps,
RangeDatePickerProps
>
) {
if (props.mode !== undefined) {
switch (props.mode) {
case "single":
return <SingleDatePicker {...props} />;
case "range":
return <RangeDatePicker {...props} />;
}
}
const defaultProps = props as DatePickerProps;
return <SingleDatePicker {...defaultProps} />;
}
This way, it would be a lot harder for our consumer to misuse our component.