Home > Net >  React custom component with conditional type
React custom component with conditional type

Time:09-18

My target is to allow other developers to apply a click handler for a button, on click event, only if the button's type is button.

User can set only button or submit type.

I don't want to allow developer to set onClick property on the component when type is submit.

I have the following custom component:

import React, { type ReactNode, type ButtonHTMLAttributes } from 'react';

import { concatDiverseClasses } from '@/utils/component';

import EDSvg from '../EDSvg';
import type icons from '../../../assets/icons';

import classes from './EDAcceptButton.module.scss';

interface IBaseProps<
    ButtonType = Extract<ButtonHTMLAttributes<HTMLButtonElement>['type'], 'button' | 'submit'>,
> {
    readonly className?: string;
    readonly type: ButtonType;
    readonly disabled: boolean;
    readonly iconName?: keyof typeof icons;
    readonly children?: ReactNode;
}

type IProps<ButtonType = Extract<ButtonHTMLAttributes<HTMLButtonElement>['type'], 'button' | 'submit'>> =
    ButtonType extends 'submit'
        ? IBaseProps
        : IBaseProps & {
                readonly onClick: VoidFunction;
          };

const EDAcceptButtonView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
    if (props.type === 'submit') {
        return (
            <button
                className={concatDiverseClasses(classes['container'], props.className)}
                type="submit"
                disabled={props.disabled}
            >
                {props.iconName ? (
                    <>
                        <span className={classes['container__text']}>{props.children}</span>
                        <EDSvg className={classes['container__icon']} name={props.iconName} />
                    </>
                ) : (
                    props.children
                )}
            </button>
        );
    }

    return (
        <button
            className={concatDiverseClasses(classes['container'], props.className)}
            type="button"
            disabled={props.disabled}
            onClick={props.onClick}
        >
            {props.iconName ? (
                <>
                    <span className={classes['container__text']}>{props.children}</span>
                    <EDSvg className={classes['container__icon']} name={props.iconName} />
                </>
            ) : (
                props.children
            )}
        </button>
    );
};

EDAcceptButtonView.displayName = 'EDAcceptButtonView';
EDAcceptButtonView.defaultProps = {};

export default React.memo(EDAcceptButtonView);

But I get an error

Property 'onClick' does not exist on type 'PropsWithChildren<IBaseProps<"submit" | "button"> | (IBaseProps<"submit" | "button"> & { readonly onClick: VoidFunction; })>'.
  Property 'onClick' does not exist on type 'IBaseProps<"submit" | "button"> & { children?: ReactNode; }'.ts(2339)

Also, when I try to use this component outside:

                        <EDAcceptButton
                            className={classes['formActions__submit']}
                            type="submit"
                            disabled={!props.isSecretLabelValid}
                            onClick={() => {}}
                        >

Typescript allows me to apply onClick propety. Why?

CodePudding user response:

The problem is that you don't pass a type argument to IBaseProps, which means that each of the object types in the union created by IProps has a type property that is a union (and can't be used to discriminate).

If you pass the ButtonType to IBaseProps, it works as expected:

type IProps<ButtonType = Extract<ButtonHTMLAttributes<HTMLButtonElement>['type'], 'button' | 'submit'>> =
    ButtonType extends 'submit'
        ? IBaseProps<ButtonType>
        : IBaseProps<ButtonType> & {
                readonly onClick: VoidFunction;
          };

TypeScript playground (has some errors due to missing imports, but these don't affect the solution)

  • Related