简体   繁体   中英

React and Typescript functional component can be 1 of 3 interfaces

I'm trying to create a component that can use 1 of 3 interfaces that is able to determine what interface based on what props are passed to it.

interface CommonProps {
  label: string;
  icon?: React.ComponentType;
  role?: string;
}

interface ButtonProps extends CommonProps {
  handleOnClick: () => void;
  selected?: boolean;
  largeVariant?: boolean;
}

interface LinkProps {
  to: string;
  openInNewTab?: boolean;
}

interface HrefProps {
  href: string;
  openInNewTab?: boolean;
}

const Button: React.FC<ButtonProps | LinkProps | HrefProps> = props => {
  const { label, handleOnClick, to, href, icon, openInNewTab } = props;
  if (to || href) {
    const Component = to ? Link : 'a';
    return (
      <StyledButton
        component={Component}
        target={openInNewTab ? '_blank' : undefined}
        onMouseDown={(e: any) => {
          href && pushMatomoExternalLink(e, href);
        }}
        {...props}
      >
        {icon && <StyledIcon icon={icon} />}
        {label}
      </StyledButton>
    );
  }
  return (
    <StyledButton onClick={handleOnClick} {...props}>
      {icon && <StyledIcon icon={icon} />}
      {label}
    </StyledButton>
  );
};

Desired Behavior including errors I would expect to see.

<Button label="View Report" handleOnClick={action('BUTTON CLICKED')} />

Would infer the interface is ButtonProps

<Button label="View Report" selected />

TypeScript error: Property 'handleOnClick' is missing in type '{ label: string; selected: boolean;}' but required in type 'ButtonProps'.

<Button label="View Report" openInNewTab />

Would infer that interface would be LinkProps or HrefProps

Property 'to' is missing in type '{ label: string; openInNewTab: boolean; }' but required in type 'LinkProps'.

Property 'href' is missing in type '{ label: string; openInNewTab: boolean; }' but required in type 'HrefProps'.

<Button label="View Report" href="/" openInNewTab />

Would infer the interface is HrefProps

I don't understand what your issue is; if I take your exact code except for the errorful implementation of Button (assuming that this implementation is not needed for a minimum reproducible example ), the compiler gives exactly the errors you want to see

declare const Button: React.FC<ButtonProps | LinkProps | HrefProps>;
<Button label="View Report" handleOnClick={console.log} />;
// okay

<Button label="View Report" selected />; 
// Property 'handleOnClick' is missing in type '{ label: string; selected: true; }' 
// but required in type 'ButtonProps'.(2322)

<Button label="View Report" openInNewTab />;
// Property 'href' is missing in type '{ label: string; openInNewTab: true; }' 
// but required in type 'HrefProps'.(2322)

<Button label="View Report" href="/" openInNewTab />;
// okay

If this is not as you expect, or if the issue is actually with the implementation of Button , then please edit your question to specify what your issue is, including a reproducible example that shows exactly what you are seeing and how it differs from what you expect to see. Good luck!

Playground link to code

The one possible solution is to use type guards to check what type of props is passed. A type guard should have a logic to find out what type of passed props it got. In the code below propsIsButtonProps , propsIsLinkProps and propsIsHrefProps are type guards.

I have simplified your code to showcase the main idea.

function propsIsButtonProps (props: any): props is ButtonProps {
  return props.selected !== undefined;
}
function propsIsLinkProps (props: any): props is LinkProps {
  return props.to !== undefined;
}
function propsIsHrefProps (props: any): props is HrefProps {
  return props.href !== undefined;
}

export const Button: React.FC<ButtonProps | LinkProps | HrefProps> = props => {
  if (propsIsLinkProps(props) || propsIsHrefProps(props)) {
    const Component = propsIsLinkProps(props) ? 'div' : 'a';
    return (
      <Component
        target={props.openInNewTab ? '_blank' : undefined}
        onMouseDown={(e: any) => {
          propsIsHrefProps(props) && pushMatomoExternalLink(e, props.href);
        }}
        {...props}
      >
        {props.label}
      </Component>
    );
  }
  return (
    <button onClick={props.handleOnClick} {...props}>
      {props.label}
    </button>
  );
};

Working demo is here

You are talking about function overloading, right ? This can be easily done in languages like C / C++, but with typescript it is kind of hard. This is because at the end typescript code converted in to JavaScript and in JavaScript function overloading is impossible.

Here's a possible solution for the scenario.

interface CommonProps {
  label: string;
  icon?: string;
  role?: string;
}

export interface ButtonProps extends CommonProps {
  kind: "button-props";
  handleOnClick: () => void;
  selected?: boolean;
  largeVariant?: boolean;
}

export interface LinkProps {
  kind: "link-props";
  to: string;
  openInNewTab?: boolean;
}

export interface HrefProps {
  kind: "href-props";
  href: string;
  openInNewTab?: boolean;
}

type Types = HrefProps | LinkProps | ButtonProps ;

const Button : React.FC<Types> = props => {
    switch(props.kind){
        case "button-props" :
            // some code
        case "href-props" :
            // some code
        case "link-props" :
            // some code
        default :
            return;
    }
}

And now in your parent component,

import { ButtonProps, LinkProps, HrefProps } from './Button.component.ts';

function Parent = (props) => {
   const propsToChild : ButtonProps = {// data object with type ButtonProps}
   return (
     <Button {...propsToChild}/>
   )
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM