React Typescript: exclude property in higher order component

Viewed 1404

I have a HOC that provides one property to a wrapped component.

I want to write Typescript type definitions that properly expose Props type of the component created by this HOC: basically copy (infer) type of props of the wrapped component and remove the property my HOC provided. Is it possible?

In the example below, I have a HOC withBar that provides the bar:string prop to any wrapped component that accepts it. Therefore the Foo = withBar(FooBar) component should have props FooBarProps excluding the bar, which means only {foo: string}. However Typescript still apparently thinks Foo component should also get the foo property.

I use Typescript 3.3.1.

import * as React from "react";

interface FooBarProps {
 foo: string;
 bar: string;
}

class FooBar extends React.PureComponent<FooBarProps> {
 public render() {
  return <>{this.props.foo} {this.props.bar}</>;
 }
}

export function withBar<TProps>(
    WrappedComponent: React.ComponentType<TProps & {bar: string}>
) {
 return class WithBar extends React.PureComponent<TProps> {

  public render() {
   return <WrappedComponent
    {...this.props}
    bar={"BAR"}
   />;
  }
 };
}

const Foo = withBar(FooBar);

// Typescript complains here:
// Error:(29, 14) TS2741: Property 'bar' is missing in type
// '{ foo: string; }' but required in type 'Readonly<FooBarProps>'.
const foo = <Foo foo="FOO" />;

3 Answers

What about something like the following, it works, but maybe doesn't answer your question completely. https://codesandbox.io/embed/peaceful-star-194g5?fontsize=14

The gist of it is splitting up Foo and Bar into separate types and joining them together for the wrapped component. Here's the code if you don't want to visit the sandbox:

import * as React from "react";

interface BarProps {
  bar: string;
}

interface FooProps {
  foo: string;
}

type FooBarProps = FooProps & BarProps;

class FooBar extends React.PureComponent<FooBarProps> {
  public render() {
    return (
      <>
        {this.props.foo} {this.props.bar}
      </>
    );
  }
}

export function withBar<TProps>(
  WrappedComponent: React.ComponentType<TProps & BarProps>
) {
  return class WithBar extends React.PureComponent<TProps> {
    public render() {
      return <WrappedComponent {...this.props} bar={"BAR"} />;
    }
  };
}

const Foo = withBar(FooBar);

// Typescript complains here:
// Error:(29, 14) TS2741: Property 'bar' is missing in type
// '{ foo: string; }' but required in type 'Readonly<FooBarProps>'.
export const Foof = () => <Foo foo="FOO" />;

The Pick and Exclude types can help here:

interface Foo{
    woo: string
    doo: number
}
interface Goo{
    doo: number
}


type FooExcludeGoo = Pick<Foo, Exclude<keyof Foo, keyof Goo>> // {woo:string}

The best solution I've finally found:

export function withBar<TProps extends {bar: string}>(
    WrappedComponent: React.ComponentType<TProps>
) {
    return class WithBar extends React.PureComponent<
        Pick<TProps, Exclude<keyof TProps, "bar">> // = Omit<TProps, "bar">
    > {
        public render() {
            return <WrappedComponent
                {...this.props as any /* still can't do better than using any here */}
                bar={"BAR"}
            />;
        }
    };
}

The idea is that the generic argument TProps includes the property/ies I want to exclude from the result component and I exclude them manually in the resulting component's type signature.

The only caveat is that Typescript is unable to determine that {...this.props} passed to the wrapped component have a proper type, so I had to use any, but luckily it affects only internals of the component and all types exposed outside should be exact.

The Typescript error message is also going pretty wild there so I wasn't able to debug the reason: TS2322: Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<TProps, Exclude<keyof TProps, "bar">>> & { bar: string; }' is not assignable to type 'IntrinsicAttributes & TProps & { children?: ReactNode; }'. Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<TProps, Exclude<keyof TProps, "bar">>> & { bar: string; }' is not assignable to type 'TProps'.

Related