Optional generic in React component

Viewed 157

I have two problems with the following code.

interface MyForm {
  age: number;
  email: string;
  name: string;
}

function Form<
  T,
  ComponentProps extends {
    name: string;
    onChange: (event: React.ChangeEvent) => void;
  }
>(
  props: {
    component:
      | React.ComponentClass<ComponentProps>
      | React.ComponentType<ComponentProps>;
    name: keyof T;
  } & Omit<ComponentProps, "name" | "onChange">
) {
  const { component: Component, name, ...rest } = props;

  const handleChange = () => {
    //
  };

  return <Component {...(rest as any)} name={name} onChange={handleChange} />;
}

function Component(props: {
  name: string;
  onChange: (event: React.ChangeEvent) => void;
  color: "blue" | "yellow";
}) {
  const { color, name, onChange } = props;
  return <input name={name} onChange={onChange} style={{color}} />;
}

function App() {
  return (
    <>
      {/* in this code, the name is not checked */}
      <Form color="blue" component={Component} name="something" />
      {/* in this code, the name is checked, but I must define all generics */}
      <Form<MyForm, React.ComponentProps<typeof Component>>
        color="yellow"
        component={Component}
        name="name"
      />
      {/* this doesn't work */}
      <Form<MyForm> color="blue" component={Component} name="email" />
    </>
  );
}

At first, I would like to call the Form component like this <Form<MyForm> color="blue" component={Component} name="email" /> instead of defining the second generic. In a complex component I have more properties and more generics and I don't want to enforce other programmers to define all generic only because I want that the name must match the keys in MyForm. And that's the goal, that the name must be checked by TypeScript and must be keyof MyForm. I know I can use optional generic and define the default type, but it breaks the logic and then I cannot pass color when calling Form. And I cannot define that the color is required, because the logic is, that the component must have name and onChange property and I don't care about the rest and just pass the rest to the component.

At second, I cannot get rid of the any in {...(rest as any)}. I don't know, why I am getting an error when I use only {...rest}.

2 Answers

It seems like what you are asking for in your first question is to have the user of your <Form> component to be able to specify some generic parameters, (e.g. <Form<MyFormInterface> />, and have typescript still infer the rest of them (e.g. the props of the component passed to the component property).

Unfortunately, this is not supported at the moment - type inference is an all-or-nothing proposition - either typescript infers everything, or the user specified everything. There is an extremely popular github issue asking for this exact feature. See also this answer for a more generic explanation.

there is a way just define and export function overloads

export function a<A>(value: A, value2: string) :void
export function a<A,T>(value: A, value2:T) : void
export function a<A,T>(value: A, value2:T) {
  console.log(value, value2)
}
//throw error
a<string>("", 3)
//good
a<string>("","")
//throw error
a<string, number>("","")
//good
a<string, number>("",2)
Related