Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a React higher-order component with TypeScript

I'm writing a React higher-order component (HOC) with TypeScript. The HOC should accept one more prop than the wrapped component, so I wrote this:

type HocProps {
    // Contains the prop my HOC needs
    thingy: number
}
type Component<P> = React.ComponentClass<P> | React.StatelessComponent<P>
interface ComponentDecorator<TChildProps> {
    (component: Component<TChildProps>): Component<HocProps & TChildProps>;
}
const hoc = function<TChildProps>(): (component: Component<TChildProps>) => Component<HocProps & TChildProps) {
    return (Child: Component<TChildProps>) => {
        class MyHOC extends React.Component<HocProps & TChildProps, void> {
            // Implementation skipped for brevity
        }
        return MyHOC;
    }
}
export default hoc;

In other words, hoc is a function that yields the actual HOC. This HOC is (I believe) a function that accepts a Component. Since I don't know in advance what the wrapped component will be, I'm using a generic type TChildProps to define the shape of the props of the wrapped component. The function also returns a Component. The returned component accepts props for the wrapped component (again, typed using the generic TChildProps) and some props it needs for itself (type HocProps). When using the returned component, all of the props (both HocProps and the props for the wrapped Component) should be supplied.

Now, when I attempt to use my HOC, I do the following:

// outside parent component
const WrappedChildComponent = hoc()(ChildComponent);

// inside parent component
render() {
    return <WrappedChild
                thingy={ 42 } 
                // Prop `foo` required by ChildComponent
                foo={ 'bar' } />
}

But I get a TypeScript error:

TS2339: Property 'foo' does not exist on type 'IntrinsicAttributes & HocProps & {} & { children? ReactNode; }'

It seems to me TypeScript is not replacing TChildProps with the shape the of the props needed for ChildComponent. How can I make TypeScript do that?

like image 948
mthmulders Avatar asked Apr 28 '17 12:04

mthmulders


3 Answers

A bit late to the party. I like to use the Omit TypeScript utility type to solve this issue. Link to the documentation: https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk

import React, {ComponentType} from 'react';

export interface AdditionalProps {
    additionalProp: string;
}

export function hoc<P extends AdditionalProps>(WrappedComponent: ComponentType<P>) : ComponentType<Omit<P, 'additionalProp'>> {
    const additionalProp = //...
    return props => (
        <WrappedComponent
            additionalProp={additionalProp}
            {...props as any}
        />
    );
}
like image 90
ParkourGrip Avatar answered Nov 10 '22 16:11

ParkourGrip


If what you're asking for is if it's possible to define a HOC that can add a new prop, let's say "thingy", to a component without modifying that component's props definition to include "thingy" I think that's impossible.

That's because at some point in the code you'll end up with:

render() {
    return (
        <WrappedComponent thingy={this.props.thingy} {...this.props}/>
    );
}

And that will always throw an error if WrappedComponent does not include thingy in its props definition. The child has to know what it receives. Off hand, I can't think of a reason for passing a prop to a component that doesn't know about to it anyway. You wouldn't be able to reference that prop in the child component without an error.

I think the trick is to define the HOC as a generic around the props of the child and then to just include your prop thingy or whatever in that child's interface explicitly.

interface HocProps {
    // Contains the prop my HOC needs
    thingy: number;
}

const hoc = function<P extends HocProps>(
    WrappedComponent: new () => React.Component<P, any>
) {
    return class MyHOC extends React.Component<P, any> {
        render() {
            return (
                <WrappedComponent {...this.props}/>
            );
        }
    }
}
export default hoc;

// Example child class

// Need to make sure the Child class includes 'thingy' in its props definition or
// this will throw an error below where we assign `const Child = hoc(ChildClass)`
interface ChildClassProps {
    thingy: number;
}

class ChildClass extends React.Component<ChildClassProps, void> {
    render() {
        return (
            <h1>{this.props.thingy}</h1>
        );
    }
}

const Child = hoc(ChildClass);

Now of course this example HOC doesn't really do anything. Really HOC's should be doing some sort of logic to provide a value for the child prop. Like for example maybe you have a component that displays some generic data that gets updated repeatedly. You could have different ways it gets updated and create HOC's to separate that logic out.

You have a component:

interface ChildComponentProps {
    lastUpdated: number;
    data: any;
}

class ChildComponent extends React.Component<ChildComponentProps, void> {
    render() {
        return (
            <div>
                <h1>{this.props.lastUpdated}</h1>
                <p>{JSON.stringify(this.props.data)}</p>
            </div>
        );
    }
}

And then an example HOC that just updates the child component on a fixed interval using setInterval might be:

interface AutoUpdateProps {
    lastUpdated: number;
}

export function AutoUpdate<P extends AutoUpdateProps>(
    WrappedComponent: new () => React.Component<P, any>,
    updateInterval: number
) {
    return class extends React.Component<P, any> {
        autoUpdateIntervalId: number;
        lastUpdated: number;

        componentWillMount() {
            this.lastUpdated = 0;
            this.autoUpdateIntervalId = setInterval(() => {
                this.lastUpdated = performance.now();
                this.forceUpdate();
            }, updateInterval);
        }

        componentWillUnMount() {
            clearInterval(this.autoUpdateIntervalId);
        }

        render() {
            return (
                <WrappedComponent lastUpdated={this.lastUpdated} {...this.props}/>
            );
        }
    }
}

Then we could create a component that updates our child once every second like this:

const Child = AutoUpdate(ChildComponent, 1000);
like image 20
KhalilRavanna Avatar answered Nov 10 '22 18:11

KhalilRavanna


I found one way to make it work: by invoking the hoc with the type argument supplied, like so:

import ChildComponent, { Props as ChildComponentProps } from './child';
const WrappedChildComponent = hoc<ChildComponentProps>()(ChildComponent);

But I don't really like it. It requires me to export the props of the child (which I'd rather not do) and I have the feeling I'm telling TypeScript something it should be able to infer.

like image 30
mthmulders Avatar answered Nov 10 '22 18:11

mthmulders