Search code examples
javascriptreactjstypescriptlogictyping

TypeScript Logic conundrum - A AND EITHER B or C or D


I'm trying to set up a type for a custom input component that will inherit properties based on the type of element to be displayed. Essentially the logic is what the title states

type A AND EITHER B or C or D or E

meaning A is always applied, then either B or C or D or E follow. Here's what I have so far.

 interface GenericInputElementProps {
  id: string;
  hidden: boolean;
  label: string;
  onInput: (
    id: string,
    value: string | number,
    isValid: boolean
  ) => {
    type: string;
    value: string | number;
    inputId: string;
    isValid: boolean;
  };
  validators?: { type: string; configVal?: number }[];
  initialValue?: {
    initialValue: string;
    initialValid: boolean;
  };
}

type InputElementProps =
  | (
      | GenericInputElementProps
      | {
          element: 'input';
          type: string;
          placeholder: string;
          errorText: string;
        }
    )
  | {
      element: 'textarea';
      rows: number;
      placeholder: string;
      errorText: string;
    }
  | { element: 'number'; type: 'number' }
  | { element: 'checkbox'; type: 'checkbox' }
  | { element: 'select'; sizes: ISizes[] };

but this doesn't work because typescript thinks I'm using GenericInputElementProps and element='input' but doesn't consider the other options. I'm sure there is a way around this but I cannot find out what pattern I can use.


Solution

  • If you have types A, B, C, D, and E, and you want to represent "A and something which is either B or C or D or E", then you can do so by turning "and" into an intersection and "or" into a union, like:

    type Okay = A & (B | C | D | E);
    // type Okay = A & (B | C | D | E)
    

    Those parentheses are important, because intersections bind more tightly than unions in TypeScript syntax:

    type Oops = A & B | C | D | E;
    // type Oops = (A & B) | C | D | E
    

    That implies you should do something like:

    type InputElementProps = GenericInputElementProps & (
        {
            element: 'input';
            type: string;
            placeholder: string;
            errorText: string;
        } | {
            element: 'textarea';
            rows: number;
            placeholder: string;
            errorText: string;
        } | {
            element: 'number'; type: 'number'
        } | {
            element: 'checkbox'; type: 'checkbox'
        } | {
            element: 'select'; sizes: ISizes[]
        }
    );
    

    If you want to break those apart into their own interfaces it's up to you:

    interface TextElementProps {
        element: 'input';
        type: string;
        placeholder: string;
        errorText: string;
    };
    interface TextAreaElementProps {
        element: 'textarea';
        rows: number;
        placeholder: string;
        errorText: string;
    };
    interface NumberElementProps {
        element: 'number';
        type: 'number';
    };
    interface CheckboxElementProps {
        element: 'checkbox';
        type: 'checkbox';
    };
    interface SelectElementProps {
        element: 'select';
        sizes: ISizes[];
    };
    
    type InputElementProps = GenericInputElementProps & (
        TextElementProps |
        TextAreaElementProps |
        NumberElementProps |
        CheckboxElementProps |
        SelectElementProps
    );
    

    Playground link to code