javascriptreactjstypescriptrecompose

Correct Typing for HigherOrderComponents with recompose and typescript


I'm currently trying to get recompose into my react codebase. Therefore I was trying to get some basic things working, and I got it working, but I'm not really sure, if this is the correct way recompose is intended to work.

So I have following Code:

interface Value {
  value: string
}

interface Loading {
  loading: boolean
}

const TestComponent = (props: Value) => <div>Value: {props.value}</div>
const LoadingComponent = () => <div>Loading ...</div>

So I have a TestComponent, which should display the Value provided in the props, and additionaly I have a LoadingComponent, which should be shown, when the loading props is set.

So I used the branch function of recompose

const withLoading = branch<Loading>(
  ({loading}) => loading, 
  renderComponent(LoadingComponent)
) 

Now when I use withLoadingon any Component without props I can set the loading prop on them.

const EnhancedCompWithLoading = withLoading(SomeCompWithoutProps)

render() {
  return <EnhancedCompWithLoading loading={this.state.loading} />
}

This works fine for me, the real problem starts when trying to use this with Components with props. When I try it like this:

const TestWithLoading = withLoading(TestComponent)

render() {
  return <TestWithLoading value="testVal" loading={this.state.loading} />
}

I get the ErrorMessage TS2339: Property 'value' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<Loading, any, any>> & Readonly<{ children?: ReactNode; }> & Readonly<Loading>'.

So I looked the type definition in @types/recompose up. branch<TOutter> returns a ComponentEnhancer<any,TOutter>. Which I understand, I want to be able to provide any component, and the <TOutter> generic is so, that the resulting component knows about the needed props for the test function. Thats also working without additionals props.

However the TypeDefinition for the ComponentEnhancer looks like this (recompose 0.30.2):

interface ComponentEnhancer<TInner, TOutter> {
  (component: Component<TInner>): ComponentClass<TOutter>
}

So, the ComponentEnhancer<any, Loading> which I received from the previous branch function will return a ComponentClass<Loading>. However the <TInner> of the component I provide to the ComponentEnhancer will be thrown away and I cant use my Value props in the Enhanced Component.

So my Question here is, am i doing it just wrong, is there a better way to achieve this (with recompose). Or is it just a Bug in the TypeDeclaration, since changing the return of the ComponentEnhancer to ComponentClass<TOutter & TInner> fixes the whole thing for me. Any thoughts about this?


Solution

  • I am not familiar with branch and recompose but if this is just a typing issue we can fix it.

    The problem is that the type for branch is not very good. If the intent is to forward the properties of the wrapped component to the HOC and add to the HOC the props typed explicitly to branch then a better type for the result would be:

    type BetterComponentEnhancer<TOutter> = {
        <TInner>(component: React.ComponentType<TInner>): React.ComponentClass<TInner & TOutter>
    }
    

    With this type, this will work:

    const withLoading = branch<Loading>(
        ({ loading }) => loading,
        renderComponent(LoadingComponent)
    )as unknown as BetterComponentEnhancer<Loading>
    
    type BetterComponentEnhancer<TOutter> = {
        <TInner>(component: React.ComponentType<TInner>): React.ComponentClass<TInner & TOutter>
    }
    
    const TestWithLoading = withLoading(TestComponent)
    
    function render() {
        return <TestWithLoading value="testVal" loading={true} />
    }