93

I have the following TypeScript interface:

interface SelectProps {
    options: Option[];
    value: string[];
    onChange: (value: string[]) => void;
}

I want to add boolean called isMultiple that will change the types of the other properties.

When isMultiple=true

  • enforce value:string[]
  • enforce onChange: (value: string[]) => void;

When isMultiple=false

  • enforce value:string
  • enforce onChange: (value: string) => void;

Is it possible to dynamically set the type of other properties based on the value of one property?

5 Answers 5

108

It's a bit late but I hope it helps others like it helped me.

Discriminated unions, also known as tagged unions or algebraic data types can be used to solve this problem.

interface MultipleSelectProps {
    isMultiple: true;
    options: string[];
    value: string[];
    onChange: (value: string[]) => void;
}

interface SingleSelectProps {
    isMultiple: false;
    options: string[];
    value: string;
    onChange: (value: string) => void;
}

type SelectProps = MultipleSelectProps | SingleSelectProps;

Usage example:

function Select(props: SelectProps) {
    if (props.isMultiple) {
        const { value, onChange } = props;
        onChange(value);
    } else if (props.isMultiple === false) {
        const { value, onChange } = props;
        onChange(value);
    }
}

Note: When isMultiple is undefined or null it is not possible to infer the specific type of SelectProps. In these cases is necessary to do a strict comparison isMultiple === false.

Source: https://blog.mariusschulz.com/2016/11/03/typescript-2-0-tagged-union-types

Sign up to request clarification or add additional context in comments.

7 Comments

This seems to work for the calling code but the function that accepts the SelectProps seems to not be able to differentiate the onChange call. See this code for an example.
That is because is multiple can be undefined or null. In those cases it is no possible to infer the specific type of SelectProps. Full answer here
Wonderful! If you update your answer with the usage function Select(props: SelectProps) then I will mark as the correct answer!
Is there any way to type check onChange value argument as one of options? So the onChange callback could also be validated against correct values? I guess options couldn't be dynamic in this case?
I guess I did not understood this last question. Can you provide an example?
|
54

UPDATE:

Actually, it can be done easier. All that is required here is using a union type like this:

type SelectProps = {
      isMultiple: true;
      options: string[];
      value: string[];
      onChange: (value: string[]) => void;
    } | {
      isMultiple: false;
      options: string[];
      value: string;
      onChange: (value: string) => void;
    }

You can make use of typescript's Distributive Conditional Types.

So your type will look like this:

type SelectProps<TMultiple = boolean> = TMultiple extends true
  ? {
      isMultiple: TMultiple;
      options: string[];
      value: string[];
      onChange: (value: string[]) => void;
    }
  : {
      isMultiple: TMultiple;
      options: string[];
      value: string;
      onChange: (value: string) => void;
    };

Here is a quick example.

5 Comments

This should be the accepted answer.
Can I achieve something similar if "TMultiple" is a value within an Union? I mean, if the value of "isMultiple" matches '1' | '2' or not
Yes, I updated the answer btw and all you need to do is to set values '1' or '2' instead of isMultiple true/false
Really cool! Didn't know about these conditional types. Is there any big difference between that and an union?
The "union type" is wrong in the updated example code. Currently it is as ":", but I think the correct is "|".
2

I like the @Cris response.
I just refactored it in a different way to simplify it and also corrected the union symbol (: => |)

type SelectProps<TMultiple = boolean, T = TMultiple extends true ? string[] : string> = {
      isMultiple: TMultiple;
      options: string[];
      value: T;
      onChange: (value: T) => void;
};

Comments

0

Currently (TS 5.8.2) this can be done like this

type SelectProps<TMultiple = boolean> = TMultiple extends boolean ? {
    isMultiple: TMultiple
    options: Option[]
    value: TMultiple extends true ? string[] : string
    onChange: TMultiple extends true ? (value: string[]) => void : (value: string) => void
} : never

Or

type Value<TMultiple> = TMultiple extends true ? string[] : string

type SelectProps<TMultiple = boolean> = TMultiple extends boolean ? {
    isMultiple: TMultiple
    options: Option[]
    value: Value<TMultiple>
    onChange: (value: Value<TMultiple>) => void
} : never

Comments

-6

No, you can't do that, but here are two alternatives

(1) Use union types for value and onChange:

interface SelectProps {
    options: Option[];
    isMultiple: boolean;
    value: string | string[];
    onChange: (value: string | string[]) => void;
}

(2) Use two different interfaces for the two cases:

interface SelectProps {
    options: Option[];
    isMultiple: boolean;
}

interface SingleSelectProps extends SelectProps {
    value: string;
    onChange: (value: string) => void;
}

interface MultipleSelectProps extends SelectProps {
    value: string[];
    onChange: (value: string[]) => void;
}

function isSingle(props: SelectProps): props is SingleSelectProps {
    return !props.isMultiple;
}

function isMultiple(props: SelectProps): props is MultipleSelectProps {
    return props.isMultiple;
}

(code in playground)

2 Comments

An issue with the first approach is that value could be string and onChange could be (value: string[]) => void
@AntonyO'Neill Yeah, you're right. The solution in the other (accepted) answer is better.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.