Skip to content

HOC returned component props can not differ from HOC generic props #28938

Open

Description

TypeScript Version: 3.3.0-dev.20181208

Search Terms:

  • HOC
  • react
  • higher order component

Code

import * as React from 'react';

export interface HOCProps {
  foo: number;
}

/** Remove props, that have been prefilled by the HOC */
type WithoutPrefilled<T extends HOCProps> = Pick<T, Exclude<keyof T, 'foo'>>;

function withFoo<P extends HOCProps>(WrappedComponent: React.ComponentType<P>) {
  return class SomeHOC extends React.Component<WithoutPrefilled<P>> {
    public render(): JSX.Element {
      return <WrappedComponent {...this.props} foo={0} />;
    }
  };
}

Expected behavior:
No error, like with every version below 3.2.0.

Actual behavior:
Throws an error highlighting the WrappedComponent in the render method.

[ts]
Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<P, Exclude<keyof P, "foo">>> & { foo: number; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'.
  Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<P, Exclude<keyof P, "foo">>> & { foo: number; }' is not assignable to type 'P'. [2322]

Additional Information

This is pretty much the same sample example used in #28720, but with the difference, that the props of the returned component differ from the generic.

Basically the HOC prefills the foo property for the WrappedComponent. Since the spreaded props are overriden by foo, I don’t want foo to be a valid property for the HOC. This does not seem to be possible anymore.

Playground Link:

Related Issues:
#28720

Activity

  1. flsilva commented on Dec 10, 2018

    @flsilva

    I believe I'm having the exact same problem on TypeScript 3.2.2, and I'd like to share my use case to show another way this is affecting HOCs in React. In my case I have a HOC that grabs WrappedComponent's children to render it in a different way, forwarding all props but children to WrappedComponent, resulting in the exact same error Henning Pohlmeyer (@hpohlmeyer) mentioned.

    Here's a simplified (React Native) code:

    import * as React from 'react';
    import { ViewProps } from 'react-native';
    
    export const withXBehavior = <P extends ViewProps>(
      WrappedComponent: React.ComponentType<P>,
    ): React.ComponentType<P> => (props) => {
      const { children, ...otherProps } = props;
    
      return (
        <WrappedComponent {...otherProps}> /* ERROR HERE */
          <React.Fragment>
            <View>
              <Text>example of HOC stuff here</Text>
            </View>
            {children}
          </React.Fragment>
        </WrappedComponent>
      );
    };

    Error:
    Type '{ children: Element; }' is not assignable to type 'P'. [2322]

    This was working fine on 3.0.1 and is now giving me this error on 3.2.2 (haven't tested on 3.1).

    In my real world case I need <P extends ViewProps> to have the ability to use onLayout:

    <WrappedComponent {...otherProps} onLayout={this.myFunction} />

    If I forward all HOC props to WrappedComponent the error is gone, but that would prevent HOCs to change behavior of WrappedComponents.

    import * as React from 'react';
    import { Text, View, ViewProps } from 'react-native';
    
    export const withXBehavior = <P extends ViewProps>(
      WrappedComponent: React.ComponentType<P>,
    ): React.ComponentType<P> => (props) => {
      const { children } = props;
    
      return (
        <WrappedComponent {...props}>
          <React.Fragment>
            <View>
              <Text>example of HOC stuff here</Text>
            </View>
            {children}
          </React.Fragment>
        </WrappedComponent>
      );
    };

    No errors in the above code.

  2. SpiderSkippy commented on Dec 10, 2018

    @SpiderSkippy

    I'm seeing the same thing as the original poster, but from version 3.1.6 to 3.2.2

  3. weswigham commented on Dec 10, 2018

    @weswigham
    Member

    Anders Hejlsberg (@ahejlsberg) The change is that we no longer erase generics in JSX (so we actually check these calls now), and roughly that Pick<P, Exclude<keyof P, "foo">> & { foo: P["foo"] } doesn't recombine to (or get recognized as assignable to) P. It's an unfortunate interaction with generic rest/spread, and the error we output is bad, too.

  4. hpohlmeyer commented on Dec 11, 2018

    @hpohlmeyer
    Author

    Just for the sake of completeness: In some cases we do not remove the prop from the returned component, but make it optional instead. This also fails for the same reason:

    import * as React from 'react';
    
    export interface HOCProps {
      foo: number;
    }
    
    /** From T make every property K optional */
    type Partialize<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>;
    
    /** Remove props, that have been prefilled by the HOC */
    type OptionalPrefilled<T extends HOCProps> = Partialize<T, keyof HOCProps>;
    
    export default function<P extends HOCProps>(WrappedComponent: React.ComponentType<P>) {
      return class SomeHOC extends React.Component<OptionalPrefilled<P>> {
        public render(): JSX.Element {
          return <WrappedComponent foo={0} {...this.props} />;
        }
      };
    }

    Error:

    [ts]
    Type 'Readonly<{ children?: ReactNode; }> & Readonly<Partialize<P, "foo">> & { foo: number; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'.
      Type 'Readonly<{ children?: ReactNode; }> & Readonly<Partialize<P, "foo">> & { foo: number; }' is not assignable to type 'P'. [2322]
    

    So basically it’s the same. I just wanted to provide another example where TypeScript does not recognize that the type is assignable to T. This could be harder to solve then the original one though.

  5. sveyret commented on Dec 15, 2018

    @sveyret

    It looks like the same problem as in #28884. It fails even if no spread rest.

  6. ChristianIvicevic commented on Dec 31, 2018

    @ChristianIvicevic

    Starting with 3.2 the behaviour of the spread operator for generics has changed. Apparently the type of props gets erased as a negative side effect, but you can work around that by casting it back to P using {...props as P} when spreading back into the wrapped component.

  7. OliverJAsh commented on Jan 21, 2019

    @OliverJAsh
    Contributor

    Christian Ivicevic (@ChristianIvicevic)

    Apparently the type of props gets erased as a negative side effect, but you can work around that by casting it back to P using {...props as P} when spreading back into the wrapped component.

    Is there a bug somewhere where we can track this specific problem?

  8. OliverJAsh commented on Jan 22, 2019

    @OliverJAsh
    Contributor

    Anders Hejlsberg (@ahejlsberg) The change is that we no longer erase generics in JSX (so we actually check these calls now), and roughly that Pick<P, Exclude<keyof P, "foo">> & { foo: P["foo"] } doesn't recombine to (or get recognized as assignable to) P. It's an unfortunate interaction with generic rest/spread, and the error we output is bad, too.

    Wesley Wigham (@weswigham) Is this the same as #28748?

  9. aprilandjan commented on Jan 25, 2019

    @aprilandjan

    Had to use <WrappedComponent {...this.props as any} inject={injected}/> to temporarily bypass the HOC props merge.

  10. added a commit that references this issue on Feb 11, 2019
  11. 4 remaining items

  12. satansdeer commented on Mar 3, 2021

    @satansdeer

    I've tried to do this to avoid type assertion:

    type InjectedProps = {
      foo: string
    }
    
    type PropsWithoutInjected<TBaseProps> = Omit<
      TBaseProps,
      keyof InjectedProps
    >
    
    export function withFoo<TProps extends InjectedProps>(
      WrappedComponent: React.ComponentType<
        PropsWithoutInjected<TProps> & InjectedProps
      >
    ) {
      return (props: PropsWithoutInjected<TProps>) => {
        return <WrappedComponent {...props} foo="bar" />
      }
    }

    This way the wrapped component can't receive the injected props and TypeScript does not complain.

    Here is a TypeScript playground with an example of how this HOC can be used.

  13. gabriellsh commented on Mar 4, 2021

    @gabriellsh

    Maksim Ivanov (@satansdeer) but you're not getting the rest props, it should be return ({somekey, ...props}) =>....

  14. satansdeer commented on Mar 4, 2021

    @satansdeer

    gabriellsh not sure why do you need to get the rest as you are overriding the foo prop in my example. React will accept the last value of the prop that was passed, here is a codesandbox.

  15. satansdeer commented on Mar 4, 2021

    @satansdeer

    Ok here is another take from Artem Samofalov (@dex157):

    export interface WithFooProps {
      foo: number;
    }
    
    export function withFoo<ComponentProps>(WrappedComponent: React.ComponentType<ComponentProps & WithFooProps>) {
      return class FooHOC extends React.Component<ComponentProps> {
        public render() {
          return <WrappedComponent {...this.props} foo={0} />;
        }
      };
    }

    This way we don't even have to use Omit and type inference works correctly:

    interface ButtonProps {
      title: string;
    }
    
    const Button: React.ComponentType<ButtonProps> = withFoo(({title, foo}) => {
      return <button id={String(foo)}>{title}</button>
    })
    
    
    const App = () => {
      return <main><Button title="click me" /></main>
    }

    Here is a TypeScript playground to try it out.

  16. coalaroot commented on Apr 8, 2021

    @coalaroot

    Basically the difference in Maksim Ivanov (@satansdeer) answer is to use type (which is not stated there explicitly) instead of interfaces

    This will not work

    import * as React from 'react';
    
    export interface WithFooProps {
      foo: number;
    }
    
    interface ButtonProps extends WithFooProps {
      title: string;
    }
    
    export function withFoo<ComponentProps>(WrappedComponent: React.ComponentType<ComponentProps & WithFooProps>) {
      return (props: ComponentProps) => {
        return <WrappedComponent {...props} foo={0} />;
      }
    };
    
    const RealButton: React.FC<ButtonProps> = ({title, foo}) => {
      return <button id={String(foo)}>{title}</button>
    } 
    
    const Button = withFoo(RealButton)
    
    
    const App = () => {
      return <main><Button title="click me" /></main>
    }

    And this will

    import * as React from 'react';
    
    export interface WithFooProps {
      foo: number;
    }
    
    type ButtonProps= {
      title: string;
    } & WithFooProps
    
    export function withFoo<ComponentProps>(WrappedComponent: React.ComponentType<ComponentProps & WithFooProps>) {
      return (props: ComponentProps) => {
        return <WrappedComponent {...props} foo={0} />;
      }
    };
    
    const RealButton: React.FC<ButtonProps> = ({title, foo}) => {
      return <button id={String(foo)}>{title}</button>
    } 
    
    const Button = withFoo(RealButton)
    
    
    const App = () => {
      return <main><Button title="click me" /></main>
    }

    The difference is in ButtonProps

  17. satansdeer commented on Apr 11, 2021

    @satansdeer

    Jakub Królak (@coalaroot) great catch. Yes, using the extends syntax is not the same as having an intersection type.

  18. Omelyan commented on Apr 27, 2023

    @Omelyan

    And this will

    will... how about generics in FooProps? :))

    export interface WithFooProps<T> {
      foo: T;
    }
  19. coalaroot commented on Apr 27, 2023

    @coalaroot

    Can do but why though?

  20. coalaroot commented on Apr 27, 2023

    @coalaroot

    Sorry man, don't have the time to figure it out :(

  21. hayata-suenaga commented on Jul 6, 2023

    @hayata-suenaga

    This is the simplest reproducible sample code

    type Foo = {
      foo: string;
    };
    
    function addFoo<P extends Foo>() {
      return (argWithoutFoo: Omit<P, keyof Foo>) => {
        const argWithFoo: P = { ...argWithoutFoo, foo: "foo" };
      };
    }

    This raises the following error:

    Type 'Omit<P, "foo"> & { foo: string; }' is not assignable to type 'P'.
      'Omit<P, "foo"> & { foo: string; }' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'Foo'.ts(2322)
    
  22. lifeiscontent commented on Sep 4, 2024

    @lifeiscontent

    Hayata Suenaga (@hayata-suenaga) weird thing is, this works:

    type Foo = {
      foo: string;
    };
    
    function addFoo<P extends Foo>() {
      return (argWithoutFoo: Omit<P, keyof Foo>) => {
        const argWithFoo: Omit<P, keyof Foo> & Pick<P, keyof Foo> = { ...argWithoutFoo, foo: "foo" };
      };
    }
  23. keropodium commented on May 23, 2025

    @keropodium

    In my case I had to go from this:

    export function withBaseProviders<P extends {}>(
      WrappedComponent: React.ComponentType<P>,
    ) {
      const WithBaseProviders = ({ a, b, c, d, e, ...props }: P & BaseProvidersProps) => (
        <BaseProviders a={a} b={b} c={c} d={d} e={e}>
          <WrappedComponent {...props} />
        </BaseProviders>
      );
      WithBaseProviders.displayName = `BaseProviders(${
        WrappedComponent.displayName || WrappedComponent.name || "Component"
      })`;
      return WithBaseProviders;
    }

    That raised:

    Type 'Omit<P & BaseProvidersProps, "a" | "b" | "c" | "d" | "e">' is not assignable to type 'IntrinsicAttributes & P'.
      Type 'Omit<P & BaseProvidersProps, "a" | "b" | "c" | "d" | "e">' is not assignable to type 'P'.
        'Omit<P & BaseProvidersProps, "a" | "b" | "c" | "d" | "e">' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint '{}'.ts(2322)
    

    To extend the generic type and then explicitly omit on the wrapped:

    export function withBaseProviders<P extends BaseProvidersProps>(
      WrappedComponent: React.ComponentType<Omit<P, keyof BaseProvidersProps>>,
    ) {
      const WithBaseProviders = ({ a, b, c, d, e, ...props }: P) => (
        <BaseProviders a={a} b={b} c={c} d={d} e={e}>
          <WrappedComponent {...props} />
        </BaseProviders>
      );
      WithBaseProviders.displayName = `BaseProviders(${
        WrappedComponent.displayName || WrappedComponent.name || "Component"
      })`;
      return WithBaseProviders;
    }
  24. RyanCavanaugh commented on Sep 1, 2026

    @RyanCavanaugh
    Member

    This error is correct. P extends HOCProps does not mean that P is exactly HOCProps; it can require a narrower foo type. For example, P could be { foo: 1; label: string }. The HOC supplies foo={0}, so the props assembled from WithoutPrefilled<P> and { foo: number } are not safe to pass to every possible React.ComponentType<P>.

    This is the distinction described in Why can't I narrow or assign to a generic type parameter the way I can with its constraint?. TypeScript 3.2 correctly began checking the generic JSX call instead of erasing its type parameter.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDomain: JSX/TSXRelates to the JSX parser and emitter

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions