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

Viewed 33

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.

2 Answers

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

This was what I ended up doing

  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 TextElementProps = GenericInputElementProps & {
  element: 'input';
  type: string;
  placeholder: string;
  errorText: string;
};
type TextAreaElementProps = GenericInputElementProps & {
  element: 'textarea';
  rows: number;
  placeholder: string;
  errorText: string;
};
type NumberElementProps = GenericInputElementProps & {
  element: 'number';
  type: 'number';
};
type CheckboxElementProps = GenericInputElementProps & {
  element: 'checkbox';
  type: 'checkbox';
};
type SelectElementProps = GenericInputElementProps & {
  element: 'select';
  sizes: ISizes[];
};

Then

 export const Input = (
  props:
    | TextElementProps
    | TextAreaElementProps
    | NumberElementProps
    | CheckboxElementProps
    | SelectElementProps
) => {};

Perhaps it looks a bit ridiculous but it worked splendidly.

Related