2

I use React 16 and Typescript 3. I create a component that returns button or link based on whether property to is set or not. The component may get either to or onClick property, it can't take both.

I found issue on TypeScript repository which exactly describes my problem and it seems fixed in 2.2 version, but in some strange way, it doesn't work.

For that purpose I created interfaces and use them as follows:

interface GeneralProps {/* whatever here, it works */}
interface LinkProps extends GeneralProps { to: string }
interface ButtonProps extends GeneralProps { 
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void 
  // onClick might be as well undefined
}

function Button (props: LinkProps | ButtonProps): JSX.Element {
  const Component: AnyStyledComponent = props.to ? Link : Button
  return (
    <Component to={props.to} onClick={props.onClick}>
      {props.children}
    </Component>
  )
}

Alternatively, I also tried to write this function like this:

function Button (props: LinkProps): JSX.Element
function Button (props: ButtonProps): JSX.Element {
  const Component: AnyStyledComponent = props.to ? Link : Button
  return (
    <Component to={props.to} onClick={props.onClick}>
      {props.children}
    </Component>
  )
}

The first implementation of the Button function above throws both errors, the second one throws only the first one:

Property 'to' does not exist on type 'LinkProps | ButtonProps'. Property 'to' does not exist on type 'ButtonProps'.

Property 'onClick' does not exist on type 'LinkProps | ButtonProps'. Property 'onClick' does not exist on type 'LinkProps'.

To avoid errors, I came with the silly workaround:

function Button (props: LinkProps | ButtonProps): JSX.Element {
  const properties = Object.keys(props)
  const to = properties.find((el) => el === 'to')
  const Component: AnyStyledComponent = to ? Link : Button
  return (
    <Component {...props}>
      {props.children}
    </Component>
  )
}

However, that doesn't solve my problem, because I still can pass both to and onClick property to the Button component.

Do I have some kind of an error in my code that prevents me from achieving my goal, should I approach this problem from a different angle or is this plain impossible to do?

Community
  • 1
  • 1
Dune
  • 674
  • 1
  • 9
  • 19
  • 1
    One of your problems is that [unions in TypeScript are inclusive, not exclusive](https://stackoverflow.com/a/50355289/2887218), so that a value of type `LinkProps | ButtonProps` may be a value of type `LinkProps` or one of type `ButtonProps`, *or both* (i.e., the intersection `LinkProps & ButtonProps`). – jcalz Jan 23 '19 at 00:54

1 Answers1

5

Thanks to the thread jcalz posted, I came up with a solution which actually works as I intended to. My solution is different (I use interfaces, not types), however that thread made me think of using the never type. I used it also before, but as required property, and then typescript demanded to pass a value, and when you passed it, typescript demanded to remove it. The never typed properties must be optional.

interface GeneralProps {/* whatever here, it works */}
interface LinkProps extends GeneralProps { 
  to: string
  onClick?: never
}
interface ButtonProps extends GeneralProps { 
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void 
  to?: never
}
function Button (props: LinkProps | ButtonProps): JSX.Element { ... }

With the current solution, typescript always recognises that both properties, to and onClick, exist on the type, doesn't throw an error when I pas either of this properties, but throws when I pass both properties.

Dune
  • 674
  • 1
  • 9
  • 19