Why does property spreading work inside Grid but not in FormControl?

Viewed 119

Sandbox here: https://codesandbox.io/s/agitated-ardinghelli-fnoj15?file=/src/temp4.tsx:0-1206.

import { FormControl, FormControlProps, Grid, GridProps } from "@mui/material";

interface ICustomControlProps {
  gridProps?: GridProps;
  formControlProps?: FormControlProps;
}

const CustomControl = (props: ICustomControlProps) => {
  return (
    <Grid {...props.gridProps}>
      <FormControl {...props.formControlProps} />
    </Grid>
  );
};

const CustomControlVariant1 = (props: ICustomControlProps) => {
  return (
    <CustomControl
      /**
       * Below gridProps is of type GridProps.
       * It is set to object with some fixed (item, xs, sm, md) properties and spreaded props.gridProps 
       * (which is of type GridProps). 
       * But doing exactly same with formControlProps doesnt work: setting fixed component properties
       * and spreaded props.formControlProps
       */
      gridProps={{ item: true, xs: 12, sm: 12, md: 12, ...props.gridProps }}
      formControlProps={{
        component: "fieldset",  /** ERROR: Type '{ children?: React.ReactNode; classes?: Partial<FormControlClasses> & 
                                  * Partial<ClassNameMap<never>>; color?: "error" | ... 4 more ... | "warning"; ...
                                  * 264 more ...; component: string; }' is not assignable to type 'FormControlProps<"div", {}>'.
                                  * Object literal may only specify known properties, and 'component' does not exist in type 
                                  * 'FormControlProps<"div", {}>'. 
                                  * 
                                  * Object literal may only specify known properties, and 'component' 
                                  * does not exist in type 'FormControlProps<"div", {}>'.ts(2322)
                                  */
        ...props.formControlProps
      }}
    />
  );
};

Update

Note that adding another level of spreading for FormControl works:

<CustomControl
      gridProps={{ item: true, xs: 12, sm: 12, md: 12, ...props.gridProps }}

      // this works!! So, `component` is indeed on `FormControlProps`
      // which can be confirmed here: https://mui.com/api/form-control/#props
      formControlProps={{
        ...{
          component: "fieldset",
          ...props.formControlProps
        }
      }}
    />

Here is the sandbox for this code.

I find both approaches logically correct and in my opinion, both should work, but I don't understand why the first one is giving an error.

1 Answers

TLDR;

In order to make TypeScript happy, you could modify ICustomControlProps as follows:

interface ICustomControlProps {
  gridProps?: GridProps;
  formControlProps?: FormControlProps & { component: React.ElementType };
}

Explanation

Let's have a look in the FormControl's declaration file:

declare const FormControl: OverridableComponent<FormControlTypeMap>;

export type FormControlProps<
  D extends React.ElementType = FormControlTypeMap['defaultComponent'],
  P = {},
> = OverrideProps<FormControlTypeMap<P, D>, D>;

Concisely, FormControlProps will gather together the props of the default component(which is a div - so, some props a specific DOM element can take in) and the props which define the FormControl component(these can be found in FormControlTypeMap, e.g. classes, fullWidth, hiddenLabel etc).

In FormControlTypeMap there surely is no component property defined and hence the error. However, OverridableComponent<FormControlTypeMap> says that receiving a component prop is accepted:

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;
  (props: DefaultComponentProps<M>): JSX.Element;
}

So, you can safely use FormControl like this: <FormControl component={...} />, but wrapping it in another component, like this:

interface ICustomControlProps {
  gridProps?: GridProps;
  formControlProps?: FormControlProps;
}

const CustomControl = (props: ICustomControlProps) => {
  return (
    <Grid {...props.gridProps}>
      <FormControl {...props.formControlProps} />
    </Grid>
  );
};

will require you to specify that a component prop is also allowed on ICustomControlProps.formControlProps:

interface ICustomControlProps {
  gridProps?: GridProps;
  formControlProps?: FormControlProps & { component: React.ElementType };
}

As to why this works:

formControlProps={{
  ...{
    component: "fieldset",
    ...props.formControlProps
  }
}}

the answer seems to be found in this issue, more specifically in this comment, which says that ... creates a subtype, so it's all valid.

Related