Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typescript] Incompatible types between React.forwardRef and OverridableComponent #32420

Closed
2 tasks done
HHK1 opened this issue Apr 22, 2022 · 11 comments · Fixed by #35311
Closed
2 tasks done

[typescript] Incompatible types between React.forwardRef and OverridableComponent #32420

HHK1 opened this issue Apr 22, 2022 · 11 comments · Fixed by #35311
Assignees

Comments

@HHK1
Copy link

HHK1 commented Apr 22, 2022

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the latest version

Current behavior 😯

When I try to define a wrapper for a component that uses the OverridableComponent type helper and uses React.forwardRef, I get a typescript error:

import React from "react";
import MUITypography, { TypographyTypeMap, TypographyProps } from "@mui/material/Typography";
import { OverridableComponent } from "@mui/material/OverridableComponent";


export const Typography: OverridableComponent<TypographyTypeMap> = React.forwardRef(function Typography<
  D extends React.ElementType
>(props: TypographyProps<D>, ref: React.Ref<HTMLSpanElement> | null) {
  return <MUITypography ref={ref} variantMapping={defaultVariantMapping} {...props} />;
});
Type 'ForwardRefExoticComponent<Pick<SystemProps<Theme> & { align?: "inherit" | "left" | "right" | "center" | "justify" | undefined; children?: ReactNode; classes?: Partial<TypographyClasses> | undefined; ... 5 more ...; variantMapping?: Partial<...> | undefined; } & CommonProps & Omit<...>, string | ... 1 more ... | symb...' is not assignable to type 'OverridableComponent<TypographyTypeMap<{}, "span">>'.
  Type 'ReactElement<any, string | JSXElementConstructor<any>> | null' is not assignable to type 'Element'.
    Type 'null' is not assignable to type 'ReactElement<any, any>'.ts(2322)

Expected behavior 🤔

I would expect types to be compatible

Steps to reproduce 🕹

In a typescript project define this in a file:

import React from "react";
import MUITypography, { TypographyTypeMap, TypographyProps } from "@mui/material/Typography";
import { OverridableComponent } from "@mui/material/OverridableComponent";


export const Typography: OverridableComponent<TypographyTypeMap> = React.forwardRef(function Typography<
  D extends React.ElementType
>(props: TypographyProps<D>, ref: React.Ref<HTMLSpanElement> | null) {
  return <MUITypography ref={ref} variantMapping={defaultVariantMapping} {...props} />;
});

Context 🔦

I have an internal package to isolate Mui and either re-export components directly, or wrap them.

If I only export my component without doing an explicit typing with OverridableComponent, I don't have a proper autocompletion working, which is why I'd like to use that in the first place.

If I update the OverridableComponent to the following (returning JSX.Element | null instead of just JSX.Element)

export interface OverridableComponent<M extends OverridableTypeMap> {
  <C extends React.ElementType>(
    props: {
      /**
       * The component used for the root node.
       * Either a string to use a HTML element or a component.
       */
      component: C;
    } & OverrideProps<M, C>,
  ): JSX.Element | null;
  (props: DefaultComponentProps<M>): JSX.Element | null;
}

Then the type issue disappears.

Your environment 🌎

`npx @mui/envinfo`
  System:
    OS: macOS 12.3.1
  Binaries:
    Node: 14.19.1 - ~/.nvm/versions/node/v14.19.1/bin/node
    Yarn: 1.22.15 - ~/.yarn/bin/yarn
    npm: 6.14.16 - ~/.nvm/versions/node/v14.19.1/bin/npm
  Browsers:
    Chrome: 100.0.4896.127
    Edge: Not Found
    Firefox: 97.0.1
    Safari: 15.4
  npmPackages:
    @mui/system: 5.5.0 => 5.5.0

This is not browser related.

Typescript version: 4.5.4
@types/react-dom: 17.0.14
@types/react: 17.0.43

@HHK1 HHK1 added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Apr 22, 2022
@danilo-leal danilo-leal changed the title Incompatible types between React.forwardRef and OverridableComponent [typescript] Incompatible types between React.forwardRef and OverridableComponent Apr 26, 2022
@michaldudak
Copy link
Member

Due to Typescript limitations, wrapping a generic component in a forwardRef causes the generic parameters to be lost (it's similar to what's happening when wrapping a generic component in a styled() utility, as described in https://mui.com/material-ui/guides/typescript/#complications-with-the-component-prop).

To work around this issue, cast whatever the forwardRef(...) returns to the correct type:

This should work:

import React from "react";
import MUITypography, { TypographyTypeMap, TypographyProps } from "@mui/material/Typography";
import { OverridableComponent } from "@mui/material/OverridableComponent";


export const Typography: OverridableComponent<TypographyTypeMap> = React.forwardRef(function Typography<
  D extends React.ElementType
>(props: TypographyProps<D>, ref: React.Ref<HTMLSpanElement> | null) {
  return <MUITypography ref={ref} variantMapping={defaultVariantMapping} {...props} />;
}) as OverridableComponent<TypographyTypeMap>;

Let me know if this helps.

@michaldudak michaldudak added status: expected behavior Does not imply the behavior is intended. Just that we know about it and can't work around it and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Jun 5, 2022
@michaldudak michaldudak closed this as not planned Won't fix, can't repro, duplicate, stale Jun 5, 2022
@HHK1
Copy link
Author

HHK1 commented Jun 8, 2022

Hi @michaldudak thanks for your reply 🙏

In the issue description I've outlined a change to MUI lib's OverridableComponent that solved the described issue, have you had a chance to try it?
I'm not sure why OverridableComponent is returning just a JSX.Element rather than JSX.Element | null, and when I dived into that it seems to me that it was the source of the incompatibility between ForwardRefRenderFunction and my wrapped component, rather than a generic type issue. In any case, that's rather annoying and I don't think this should be marked as "expected behaviour" (and the issue could be kept open?)

@michaldudak
Copy link
Member

Hi @HHK1, sorry for the delayed response. I took a closer look at OverridableComponent and I don't see a reason for not including null as the return type. Would you like to create a PR? Please not that we have two versions of OverridableComponent - one in @mui/material and the other in @mui/types.

@michaldudak michaldudak reopened this Jul 11, 2022
@michaldudak michaldudak removed the status: expected behavior Does not imply the behavior is intended. Just that we know about it and can't work around it label Jul 11, 2022
@tsollbach
Copy link
Contributor

tsollbach commented Dec 1, 2022

We ran into the same problem in our project and i just found this issue. I seems like there was no PR for this yet, so i took the liberty to open one #35311

@rotemee
Copy link

rotemee commented Dec 18, 2022

@michaldudak - Your solution for casting the returned value of the forwardRef solves the problem, but the issue still exists in "@mui/material": "5.11.0".

It seems that this PR didn't solve the problem.

@tsollbach
Copy link
Contributor

@michaldudak - Your solution for casting the returned value of the forwardRef solves the problem, but the issue still exists in "@mui/material": "5.11.0".

It seems that this PR didn't solve the problem.

I cannot confirm.
We can now do

import React from 'react';
import type { TypographyTypeMap, TypographyProps } from '@mui/material/Typography';
import MUITypography from '@mui/material/Typography';
import type { OverridableComponent } from '@mui/material/OverridableComponent';

export const Typography: OverridableComponent<TypographyTypeMap> = React.forwardRef(
  (props: TypographyProps, ref: React.Ref<HTMLSpanElement> | null) => (
    <MUITypography ref={ref} {...props} />
  ),
);

That does not produce a Typescript Error, which is what was fixed by the PR.
Either you have a different issue or something went wrong updating your @mui/material

@michaldudak
Copy link
Member

@rotemee Please open a new issue and provide a codesandbox with reproduction steps if you still encounter issues in this area.

@kellyrmilligan
Copy link

found this issue while trying to figure out how to properly wrap mui components. I am struggling with how to get both forwardRef and specifying a component prop to play nicely together.

// button.component.tsx
import React from "react"

import {
  ButtonTypeMap,
  Button as MuiButton,
  ButtonProps as MuiButtonProps,
} from "@mui/material"
import { OverridableComponent } from "@mui/material/OverridableComponent"
import MUITypography, {
  TypographyProps,
  TypographyTypeMap,
} from "@mui/material/Typography"

export type ButtonProps = MuiButtonProps

export const Button: OverridableComponent<ButtonTypeMap> = React.forwardRef(
  function Button<C extends React.ElementType>(
    props: MuiButtonProps<C>,
    ref: React.Ref<HTMLButtonElement> | null
  ) {
    return <MuiButton ref={ref} {...props} />
  }
) as OverridableComponent<ButtonTypeMap>

gives me TS issues if the code that uses the component is something like:

<Button component="a">link button</Button>

@kellyrmilligan
Copy link

this is the only way i've gotten it to work thus far:
#37551 (comment)

https://mui.com/material-ui/guides/typescript/#complications-with-the-component-prop

@Liboul
Copy link

Liboul commented Sep 20, 2023

I have also struggled with this while defining wrapper components.
@kellyrmilligan 's suggestion did it.

This solution also worked 👇 .

// Redecalare forwardRef
declare module "react" {
  function forwardRef<T, P = {}>(
    render: (props: P, ref: React.Ref<T>) => React.ReactNode | null
  ): (props: P & React.RefAttributes<T>) => React.ReactNode | null;
}

Very surprising.

@brionmario
Copy link

brionmario commented Oct 14, 2024

I'm composing the components in the following pattern, and if I try to add the muiName property with Typography.muiName = COMPONENT_NAME, i get an error.

Typography

import MuiCard from '@mui/material/Card';
import type {CardProps as MuiCardProps, CardTypeMap} from '@mui/material/Card';
import {OverridableComponent} from '@mui/material/OverridableComponent';
import clsx from 'clsx';
import {forwardRef} from 'react';
import type {ElementType, ReactElement, Ref} from 'react';
import './card.scss';

export type CardProps<
  C extends ElementType = ElementType,
  D extends ElementType = CardTypeMap['defaultComponent'],
  P = {},
> = {
  component?: C;
} & Omit<MuiCardProps<D, P>, 'component'>;

const Card: OverridableComponent<CardTypeMap<CardProps>> = forwardRef(
  <C extends ElementType = ElementType>(
    {className, component, onClick, elevation = 0, variant = 'outlined', ...rest}: CardProps<C>,
    ref: Ref<HTMLDivElement>,
  ): ReactElement => {
    const classes: string = clsx('oxygen-card', {'with-hover': onClick}, className);

    return (
      <MuiCard ref={ref} className={classes} onClick={onClick} elevation={elevation} variant={variant} {...rest} />
    );
  },
) as OverridableComponent<CardTypeMap<CardProps>>;

Card.muiName = 'MuiCard';

export default Card;

Error
Screenshot 2024-10-14 at 14 23 52

I've tried the following:

+ as OverridableComponent<CardTypeMap> & WithWrapperProps

WithWrapperProps has the muiName property.

❌ Breaks

Screenshot 2024-10-14 at 14 28 14

Any recommended approach to overcome this typing issue?

Will i have to augment the OverridableComponent interface?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants