Search code examples
javascriptreactjstypescripttypesdiscriminated-union

union types not being recognised in React


I have these types:

export enum LayersItemOptionsEnum {
  OPERATOR,
  HEADER,
}

type sharedTypes = {
  children: string | ReactElement;
};

type LayersItemStatic = sharedTypes & {
  label: string;
  option: LayersItemOptionsEnum;
};

type LayersItemDynamic = sharedTypes & {
  currentLayer: LayersElement;
};

export type LayersItemProps = (LayersItemDynamic | LayersItemStatic) & sharedTypes;

I am trying to use them like so:

export const LayersItem: FC<LayersItemProps> = (props): ReactElement => {
  const isHeader = props.option === LayersItemOptionsEnum.HEADER;

  const { isInEditMode } = useAppSelector((state) => state.editMode);

  const shouldRenderOptions = isInEditMode && !isHeader;

  const { selectedState } = useAppSelector((state) => state);
  const states = useAppSelector((state) => state.statesData.elements);

  return (
    <StyledLayersItem header={isHeader}>
      <Row>
        <Col span={8} offset={1 /* todo: add offset dynamically */}>
          <h1>{props.label ? props.label : props.currentLayer.name}</h1>
        </Col>
        <Col span={8} offset={4}>
          {shouldRenderOptions ? (
            <Form.Item className="form-item" initialValue={props.children}>
              <Select>
                {generateOptions({ selectedState, states, props.currentLayer }).map((value) => {
                  return (
                    <Select.Option value={value.id} key={value.id}>
                      {value.name}
                    </Select.Option>
                  );
                })}
              </Select>
            </Form.Item>
          ) : (
            <>{props.children}</>
          )}
        </Col>
      </Row>
    </StyledLayersItem>
  );
};

But I am getting the errors like this one:

Property 'label' does not exist on type 'PropsWithChildren<LayersItemProps>'.
  Property 'label' does not exist on type 'sharedTypes & { currentLayer: LayersElement; } & { children?: ReactNode; }'.

For each of the props. apart from props.children. Like it doesn't see the union in types. Or am I misunderstanding something?

Basically, if the props have label, or option, I want props to be of type LayersItemStatic & shared Types, and if there is currentLayer in props, I want them to be of type LayersItemDynamic & sharedTypes.

So what am I missing here?

I am trying to achieve something like this:

type SharedType = SharedDisplayAndEditTypes & {
  required?: boolean;
  validationMessage: string;
  name: string;
};

type TextType = {
  type: 'text';
  children: string;
};

type NumberType = {
  type: 'number';
  children: number;
};

type InputType = TextType | NumberType;

type DropdownType = {
  type: 'dropdown';
  options: string[];
  children: string;
};

type ColorType = {
  type: 'color';
  defaultValue: string;
};

export type DetailsItemEditProps = (DropdownType | InputType | ColorType) & SharedType;

Solution

  • Consider this example:

    import { ReactElement } from 'react'
    
    type LayersElement = {
        tag: 'LayersElement'
    }
    
    export enum LayersItemOptionsEnum {
        OPERATOR,
        HEADER,
    }
    
    type sharedTypes = {
        children: string | ReactElement;
    };
    
    type LayersItemStatic = sharedTypes & {
        label: string;
        option: LayersItemOptionsEnum;
    };
    
    type LayersItemDynamic = sharedTypes & {
        currentLayer: LayersElement;
    };
    
    export type LayersItemProps = (LayersItemDynamic | LayersItemStatic) & sharedTypes;
    
    declare var props: LayersItemProps;
    
    props.children // ok
    

    Only children prop is allowed because it is a common prop for each element of the union.

    See Best common type

    Since nobody know which elem of the union is actually allowed TS decides to allow you only properties which are safe for each element of the union.

    Consider this smaller example:

    type LayersItemStatic = {
        label: string;
        option: string;
    };
    
    type LayersItemDynamic = {
        currentLayer: string;
    };
    
    export type LayersItemProps = LayersItemDynamic | LayersItemStatic
    
    declare var props: LayersItemProps;
    

    Because there are no common props, you are not allowed to use any prop.

    I don't think that this type is correct:

    export type LayersItemProps = (LayersItemDynamic | LayersItemStatic) & sharedTypes
    

    Since LayersItemDynamic | LayersItemStatic is reduced to {} and LayersItemProps basically equals to sharedTypes.

    Since you already added & sharedType to both LayersItemDynamic | LayersItemStatic you need rewrite your type LayersItemProps as follow:

    import { ReactElement } from 'react'
    
    type LayersElement = {
        tag: 'LayersElement'
    }
    
    export enum LayersItemOptionsEnum {
        OPERATOR,
        HEADER,
    }
    
    type sharedTypes = {
        children: string | ReactElement;
    };
    
    type LayersItemStatic = sharedTypes & {
        label: string;
        option: LayersItemOptionsEnum;
    };
    
    type LayersItemDynamic = sharedTypes & {
        currentLayer: LayersElement;
    };
    
    const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
        : obj is Obj & Record<Prop, unknown> =>
        Object.prototype.hasOwnProperty.call(obj, prop);
    
    
    export type LayersItemProps = LayersItemDynamic | LayersItemStatic
    
    const isDynamic = (props: LayersItemProps): props is LayersItemDynamic => hasProperty(props, 'currentLayer')
    const isStatic = (props: LayersItemProps): props is LayersItemStatic => hasProperty(props, 'label')
    
    
    declare var props: LayersItemProps;
    
    if (isDynamic(props)) {
        props.currentLayer // ok
    }
    
    if (isStatic(props)) {
        props.label // ok
        props.option // ok
    
    }
    

    Playground