DEV Community

React Higher-Order Components in TypeScript made simple

Dan Homola on June 14, 2017

Note: this post was originally published on my Medium profile When refactoring a Higher-Order Component (HOC) in a TypeScript project at work, the...
Collapse
 
jonaskello profile image
Jonas Kello • Edited

Nice post, just what I was looking for!

I think you can shorten

React.ComponentClass<TOriginalProps & InjectedProps>
| React.StatelessComponent<TOriginalProps & InjectedProps>

into

React.ComponentType<TOriginalProps & InjectedProps>

at least in newer react typings.

Collapse
 
danhomola profile image
Dan Homola

Thank you for the suggestion, I'll look into it :)

Collapse
 
passionkind profile image
Bastian Kistner

Hello Dan, thank you very much for this detailed introduction to typing HOCs. I'm just migrating from flow to typescript (one reason are HOCs AND performance).

I have one question. In case DemoComponent would not require the text prop, the interface would look like this:

interface DemoProps {}

When I now use Demo or DemoWithDebug in my app (as in <Demo/> or <DemoWithDebug/>), everything works fine and Typescript does not complain. But since I have an empty interface, I thought I could remove it.

But then the compiler starts complaining that the InjectedProp props is/are missing.

I assume this is because the compiler does not understand that the injected props are coming from my HOC and that <Demo/> or <DemoWithDebug/> should provide it.

That still makes sense somehow. But we do I need an extra interface here? The following does not work. Is {} different from explicitly defining an interface?

This seems to be wrong. Or is there a short form ?
const DemoComponent = (props: {} & InjectedProps): JSX.Element

Collapse
 
danhomola profile image
Dan Homola

Hi, if I understand you correctly, you really should be able to remove the DemoProps interface altogether and write:
const DemoComponent = (props: InjectedProps): JSX.Element
What exactly does the TypeScript. compiler say?

Collapse
 
passionkind profile image
Bastian Kistner • Edited

When I try this, the compiler complains that <DemoComponent/> (when it's being used) does not provide InjectedProps. If I keep the empty interface, the compiler does not complain. I do remember having read something similar related to flow. I mean that you have to explicitly set an empty object in addition to the injected props, but I may be wrong.

Even though, I do not understand why there is is a difference between declaring the Props explicitly via an interface, which works, and simply setting const DemoComponent = (props: {} & InjectedProps): JSX.Element which does not work.

here is a link to a little sandbox: codesandbox.io/s/9zvrln93z4

if you replace Props with {} in hoc/HocA.tsx, it'll tell you the following in index.tsx:

Type '{}' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<<InjectedProps>.WithHocA> & Readonly<{ children?: ...'.
  Type '{}' is not assignable to type 'Readonly<ResultProps>'.
    Property 'message' is missing in type '{}'.

message in the case of the sandbox is part of the InjectedProps defined in hoc/withHocA.tsx

Thread Thread
 
danhomola profile image
Dan Homola

I tried replicating what you describe, but for me it works without problems even without the Props added.
codesandbox.io/s/1kkkyyjpl

Collapse
 
eldnahevitaerc profile image
archana

Hey there Dan, MadProps for this article.

Going forward with this example, how would you use ReactDOM.render with this component?

Say we continue with this:

export const Demo = clickCounted()(DemoComponent);

Then our calling code would do

ReactDOM.render(<Demo {...stuff} />, document.getElementById('container'))

And we'd have to pass in something into stuff, which is of type <TOriginalProps & InjectedProps> Is that right?

So what's the point of having injected props calculated within the HOC definition? Am I missing something? If I want to render the Demo HOC, don't I have to give it all the props it needs? I think I'm missing the point...

Collapse
 
danhomola profile image
Dan Homola

Hi, thanks for the MadProps :)

<Demo> has props of type TOriginalProps & ExternalProps (no InjectedProps).

<DemoComponent> has props of type TOriginalProps & InjectedProps.

Look at the diagram at the start of the article, it should make the prop types flow clearer :)

Collapse
 
healthycola profile image
healthycola

If you need to interact with props or states from here, the only way to do it is to specify options as functions, that take the props or states as arguments.

Trying to get this working, but having issues with it. Any suggestions? Great article btw.

Collapse
 
danhomola profile image
Dan Homola

What kind of issues are you having?

The idea is that you pass something like this to the config:

const config = {
  someSetting: props => { /* get some value or something */ }
}
Collapse
 
stunaz profile image
stunaz

Have you thought of using compose from recompose for componsing HOCs ?

I could not get my typings working using compose.

Collapse
 
danhomola profile image
Dan Homola

Unfortunately, I haven't. Reading the docs, they say it works similarly to lodash's flow, which I have used and also had problems with the types (they were too liberal, allowing for errors).

Collapse
 
glowkeeper profile image
Steve Huckle

Excellent post, Dan - tons of powerful stuff demonstrated, and a great example of an elegant HOC. Thanks for sharing!

Collapse
 
danhomola profile image
Dan Homola

Thank you so much for your kind words, it means a lot :)

Collapse
 
jaxxreal profile image
Eugeny Voronin

Thank you, Dan! That article helps me a lot!

Collapse
 
danhomola profile image
Dan Homola

Thank you, I'm glad it helped :)

Collapse
 
vcardins profile image
Victor Cardins

Excellent article, I've been looking for this for days. Thanks a ton!!

Collapse
 
danhomola profile image
Dan Homola

Thank you for your kind words, I'm glad it helped you :)