0

I am new to typescript. I use a third party package (mostly irrelevant which package, this is more of a general typescript question) as such:

export const GenerateTitle: FunctionComponent = (): JSX.Element => {
  return (
    <div>
        <span>TeSTING&nbsp;&nbsp;<Badge>Soon</Badge></span>
    </div>
  )
};
export const Patient: FunctionComponent = (): JSX.Element => {
    ...
    <SubMenu icon={<GenerateTitle />} title={<GenerateTitle />} >
        <MenuItem>About</MenuItem>
    </SubMenu>
}

The code for the third party package shows the icon props and the title props having the same type

export type Props =  React.LiHTMLAttributes<HTMLLIElement> & {
  icon?: React.ReactNode;
  title?: React.ReactNode;
  ...
  onOpenChange?: (open: boolean) => void;
};

const SubMenu: React.ForwardRefRenderFunction<unknown, Props> = (
  {
    icon,
    title,
    ...
    onOpenChange,
    ...rest
  },
  ref,
) => {

return (
    <li
      {...rest}
    >
      <div>
        {icon ? (
          <span className="pro-icon-wrapper">
            <span className="pro-icon">{icon}</span>
          </span>
        ) : null}
        {title ? <span className="pro-item-content">{title}</span> : null}
      </div>
    </div>
  </li>
 }

However, the icon prop renders the component just fine while I get an error on the title prop stating:

Type 'Element' is not assignable to type '(string & (boolean | ReactChild | ReactFragment | ReactPortal | null)) | undefined'.
  Type 'Element' is not assignable to type 'string & ReactPortal'.
    Type 'Element' is not assignable to type 'string'.  TS2322

This is confusing me because it seems the typing and code to generate both the icon prop and the title prop are identical.

1 Answer 1

1

Your props extend React.LiHTMLAttributes<HTMLLIElement>

The thing is that <li> elements already support a title attribute of type string. The browser uses this to provide little tooltips.

<ul>
  <li title="tooltip!">hover over me for a second</li>
<ul>

So React.LiHTMLAttributes<HTMLLIElement> has:

title?: string;

and then you are intersecting that with:

title?: React.ReactNode;

ReactNode is a union of a large number of things, one of which is a string. So you are basically doing:

(boolean | string | null | undefined | React.Fragment | ...) & string | undefined

The only valid intersection between both sides of the & is string | undefined. So the type is narrowed to something that is supported by all the intersected types.


So, if that's how this third party component works, that title must be a string, regardless of what type it says internally.


I think the authors intent was to support a JSX title by replacing the definition of the title prop with an entirely new one.

They probably meant to Omit that key from the li props entirely. Something like:

export type Props = Omit<React.LiHTMLAttributes<HTMLLIElement>, 'title'> & {
  icon?: React.ReactNode;
  title?: React.ReactNode;
  //...
};
Sign up to request clarification or add additional context in comments.

3 Comments

Thank so much, that makes sense, and that is what I suspected. I didn't understand typescript well enough to be certain. I think I need to fork the package to change the name of the prop... since it doesn't seem like there's a way to override the title type onReact.LiHTMLAttributes<HTMLLIElement> if you are extending it. Is that correct?
Yeah you don't want to change the built in HTML prop types. This seems like a bug in that code for sure. Also that <li { ...rest }> means that if you did pass JSX to title, then that jsx would get encoded on <li title="somerenderedjsxhere"> which is pretty weird.
Actually I take that back, the title is filtered out above that line. My bad. The code is sound, but the types are not. See my update for a potential fix.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.