简体   繁体   中英

Typescript two object union - problem with making typescript think it's the other type

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.

Use a type guard (maybe)

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.

Define the type of onChange (if possible)

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)

Eg:

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 });
}

Still problems...

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM