I encountered a really weird problem and I'm not sure if the problem is related to badly written types or it's a weird typescript thing.
So I have this props type:
type SelectAnswersProps = (
| {
onChange: Dispatch<SetStateAction<MultiSelectAnswerOptionsType | null>>;
value: MultiSelectAnswerOptionsType;
isMultiSelect: true;
}
| {
onChange: Dispatch<SetStateAction<SelectAnswerOptionsType | null>>;
value: SelectAnswerOptionsType;
isMultiSelect: false;
}
) & {
displayMode?: SelectAnswerOptionsType['displayMode'];
disabled?: boolean;
};
And then I'm trying to force typescript to read what type is being passed to onChange method.
if (isMultiSelect) {
onChange({
answers,
isHeadless,
});
} else {
onChange({
answers,
displayMode,
});
}
So the first onChange is correct as it shows its type as onChange: (value: SetStateAction<MultiSelectAnswerOptionsType>) => void but the second one I'd expect to be like in the types specified in props with isMultiSelect being false but instead type of 2nd onChange is as follows:
onChange: (value: SetStateAction<MultiSelectAnswerOptionsType> & SetStateAction<SelectAnswerOptionsType>) => void so kinda mix of both even though from props types you would say it's either one or another.
I reckon I've done something silly here. Really appreciate all your help.
Thanks
You have defined the props to be (simplified for readability):
props: { onChange: MultiSelectDispatcher, ... } | { onChange: SelectDispatcher, ... }
That means, the props can only be the one or the other, and
Typescript doesn't know which one it is, and infers that from the usage:
if( isMultiSelect ){
onChange({ answers, isHeadless }); // <-- TS thinks: probably the "MultiSelect" one
} else {
onChange({ answers, displayMode }); // <-- TS already decided "MultiSelect" earlier
}
So Typescript decides which type the whole props object probably is at some point, and sticks to that decision from there on.
As a solution you might be able to use type guards (maybe not this easy, see the "But ..." below), like:
const guardMultiSelectProps = ( theProps: unknown ): theProps is MultiSelectProps => {
return theProps.isMultiSelect;
}
if( guardMultiSelectProps( theProps ) ){
onChange({ answers, isHeadless }); // <-- TS thiks: probably the "MultiSelect" one
} else {
onChange({ answers, displayMode }); // <-- TS already decided "MultiSelect" earlier
}
But also Typescript might have inferred the type of the props even earlier, or will "suddenly" do that if you make some changes to your code later.
I suggest, if possible, not to define two alternatives for the props object, but instead define only one type for the props, with two alternative onChange handlers.
(The React props should stay consistent anyway)
E.g:
props: {
onChange: MultiSelectDispatcher | SelectDispatcher,
...
}
And also use a type guard, like:
// Note: This type guard works, but is not inherently type safe.
const guardMultiSelectOnChange = ( onChange: unknown, multiSelect: boolean ): onChange is MultiSelectDispatcher => {
return multiSelect;
}
if( guardMultiSelectOnChange( onChange, isMultiSelect )) {
onChange({ answers, isHeadless });
} else {
onChange({ answers, displayMode });
}
My last type guard guardMultiSelectOnChange makes another problem obvious:
We always assumed that the onChange function is the correct one if isMultiSelect is one or the other.
But nothing prevents someone to pass isMultiSelect: false and the MultiSelectDispatcher.
It would be better to type guard the function signature itself, instead of just stating onChange is MultiSelectDispatcher if
some other property (isMultiSelect) has some value.
But that will be a somewhat larger refactoring, outside the scope of this question, I guess.