Home > front end >  Discriminate Union doesn't work for function argument
Discriminate Union doesn't work for function argument

Time:05-27

I'm trying to make a discriminate union type for two possible component props, but it doesn't seem to work for the argument that I pass to the onClick function on this example:

type TLink = {
  label: string,
  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void,
  path: string
}

type TButton = {
  label: string,
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void,
  options: [TButtonOption, TButtonOption]
}

type TOptionalButton = TLink | TButton;

const newButton: TOptionalButton = {
  path:  'somepath',
  onClick: (e) => null,
  label: 'somelabel',
}

function handleChange(e: React.MouseEvent<HTMLAnchorElement>) {
  if(newButton.onClick) newButton.onClick(e);
}

I get a TS error when passing the "e" argument down to newButton.onClick(e):

Argument of type 'MouseEvent<HTMLAnchorElement, MouseEvent>' is not assignable to parameter of type 'MouseEvent<HTMLAnchorElement, MouseEvent> & MouseEvent<HTMLButtonElement, MouseEvent>'. ...

So, why all the props are correctly typed but not this argument?

Thanks in advance!

CodePudding user response:

Typescript doesn't know which of two types do you want to use in this particular case, as the onClick method looks the same for both variants but with different generic type of it's argument. The TS v4 can detect union type via a property in some, more simple cases, but not in this one.

Your idea looks reasonable: if there is a path property, consider it as TLink, otherwise it's TButton. But the TS thinks a bit different. It gets the typed object which might contain path or options, and tries to validate, which of two properties can be used here. To do that it needs implicitly know what generic type do you gonna use in the onClick, what is not clear in your current solution.

You have to give the TS any hint to help it understand what do you really expect here.

One of possible solutions

interface TBaseButton<T extends HTMLAnchorElement | HTMLButtonElement> {
  label: string;
  onClick?: (e: React.MouseEvent<T>) => void;
}

interface TLink extends TBaseButton<HTMLAnchorElement> {
  path: string;
}

interface TButton extends TBaseButton<HTMLButtonElement> {
  options: [TButtonOption, TButtonOption];
}

type TOptionalButton<T extends HTMLAnchorElement | HTMLButtonElement> = T extends HTMLAnchorElement
  ? TLink
  : TButton;

const newButton: TOptionalButton<HTMLAnchorElement> = {
  path: 'somepath',
  onClick: (e) => null,
  label: 'somelabel',
};

function handleChange(e: React.MouseEvent<HTMLAnchorElement>) {
  if (newButton.onClick) newButton.onClick(e);
}

Another solution, you can make onClick argument more abstract so it will depend on the input type. It will help to solve the onClick argument intersection, but won't help TS to define the newButton exact type, you will see only common properties:

interface TBaseButton<T extends HTMLAnchorElement | HTMLButtonElement> {
  label: string;
  onClick: <T>(e: React.MouseEvent<T>) => void;
}

interface TLink extends TBaseButton<HTMLAnchorElement> {
  path: string;
}

interface TButton extends TBaseButton<HTMLButtonElement> {
  options: [TButtonOption, TButtonOption];
}

type TOptionalButton = TLink | TButton;

const newButton: TOptionalButton = {
  path: 'somepath',
  onClick: (e) => null,
  label: 'somelabel',
};

function handleChange(e: React.MouseEvent<HTMLAnchorElement>) {
  if (newButton.onClick) newButton.onClick(e);
}

CodePudding user response:

You need to check if 'path' exists in newButton. Update your handleChange function in the following way:

function handleChange(e: React.MouseEvent<HTMLAnchorElement>) {
  if (newButton.onClick && 'path' in newButton) newButton.onClick(e);
}

Working example from TS Playground

  • Related