Home > Blockchain >  Enforced properties on a React component with TypeScript
Enforced properties on a React component with TypeScript

Time:10-20

I would like to enforce properties on a React component with TypeScript, but I am getting weird behaviour. Bellow I am pasting only simple examples:

function FunctionalComponent(props: { color: string }) {
  return <></>;
}

type ComponentWithName<I extends React.FunctionComponent<{ name: string } & React.ComponentProps<I>>> = I;
const component: ComponentWithName<typeof FunctionalComponent> = FunctionalComponent;

The code above will pass even I declared that the component must have a property name. With this code I need to get an errror, because the FunctionalComponent does not include a name property.

On the other hand, this works:

function FunctionalComponent(props: { color: string }) {
  return <></>;
}

type ComponentWithName<I extends React.FunctionComponent<{ name: string }>> = I;
const component: ComponentWithName<typeof FunctionalComponent> = FunctionalComponent

This code will throw a TypeScript error, exactly what I need. But the issue is, that the FunctionalComponent can not have additional properties unless I add them manually to the React.FunctionComponent.

The goal is to enforce a component to have the "name" property, but allow to have more additional (not specified) properties.

I am using TypeScript version 4.4.4 and React version 17.0.2

Edit:

The true use case is this:

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

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

  return (
    <div>
      {Component ? (
        <Component
          {...(rest as React.ComponentProps<I>)}
          onChange={handleChange}
        />
      ) : (
        <input onChange={handleChange} />
      )}
    </div>
  );
}

class ComponentClass extends React.Component<{
  color: "blue" | "yellow";
}> {
  render() {
    return (
        <input style={{color: this.props.color}} />
    );
  }
}

function ComponentFunction(props: { color: "blue" | "yellow" }) {
  return <input style={{color: props.color}} />;
}

function App() {
  return (
    <>
      <Component component={ComponentClass} color="blue" />
      <Component component={ComponentFunction} color="blue" />
    </>
  );
}

The <Component component={ComponentClass} color="blue" /> will throw an type error but the <Component component={ComponentFunction} color="blue" /> does not. I need to enforce passed components to have the onChange property with the specified type.

CodePudding user response:

I may be missing something, but do you not just need to enforce the type of the Props rather than create a typed component?

interface NameProps {
   name: string;
}

type NamedComponent<T extends NameProps> = (props: T) => JSX.Element;

const notANamedComponent: NamedComponent<{ int: number }> // ...this will give you an error
const aNamedComponent: NamedComponent<{ int: number; name: string}> //OK

CodePudding user response:

The issue is that passing extra fields to the component is always valid, if you want to require it to do something with it is harder to type.

For instance this is valid code:

// from this context the argument will be called with an argument with both a and b properties
function takeF(f: (data: {a:string, b:number})=>any){}

// this function takes an object with an a property, but passing other properties would still be valid
function f(data: {a:string}){}

// this is allowed because passing an object with extra fields is still valid.
takeF(f)

The reason you are getting an error with {name: string} and {color:string} is because those have no overlap so typescript does give you errors, so the solution is to constrain your generic to what you actually need.

declare function Component<
  ComponentProps extends { onChange: (event: React.ChangeEvent) => void; }
>(
  props: {
    component?: React.ComponentClass<ComponentProps> | React.ComponentType<ComponentProps>;
  } & Omit<ComponentProps, "onChange">
): any

this way if the component doesn't have an onChange then there is no overlap and you get the error you are expecting and if there are extra properties it is fine because those are already being captured by the generic behaviour. Also note this is basically the same thing that @Marcus is saying, just constrain the generic to what you actually need.

  • Related