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)