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?
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.
Your problem is that hoc
is generic, but takes no parameters—there’s no data to infer anything from. That forces you to explicitly specify the type for hoc
when you call it. Even if you do hoc()(Foo)
, Typescript has to evaluate hoc()
first—and there’s zero information to go on at that point. Hence needing hoc<FooProps>()(Foo)
.
If the body of hoc
doesn’t require the type and can be flexible on that point, you could make hoc
not be generic, but instead return a generic function (the higher-order component).
That is, instead of
const hoc = function<TChildProps>(): (component: Component<TChildProps>) => Component<HocProps & TChildProps) {
return (Child: Component<TChildProps>) => {
you could instead have
const hoc = function(): <TChildProps>(component: Component<TChildProps>) => Component<HocProps & TChildProps) {
return <TChildProps>(Child: Component<TChildProps>) => {
(note the placement of <TChildProps>
)
Then when you call hoc()
, you get a generic function, and when you call hoc()(Foo)
, it will be inferred to be equivalent to hoc()<FooProps>(Foo)
.
This does not help you if hoc
itself needs to do something with the type parameter—if it does, you are saying “give me a function that will only accept arguments that are components with this type as a property,” and you need to give it that type then. Whether or not this helps depends on what the actual hoc
function is doing, which isn’t demonstrated in your question.
I assume that your actual use-case is doing something in hoc
, rather than just immediately returning the higher-order component function. Certainly, if not, you should just remove the layer of indirection and have there just be the higher-order component. Even if you are doing things inside hoc
, I would strongly suggest that there is a pretty good chance you are better off just not doing that, moving that functionality to within the actual higher-order component, and eliminating the indirection. Your approach only makes sense if you are calling hoc
once or a few times, but then using the resulting hoc()
function(s) many, many times with different parameters—and even then, this would be an optimization that may well be premature.