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?
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}
/>
);
}
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);
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With