I'm wondering if I have the correct typing for an API HOC's return type in the following scenario:
I have an Authentication HOC,
withAuthentication, which injects auth services into a component's props.I have an API HOC,
withRestApi, which injects API calls, and which itself useswithAuthentication.MyComponentneeds to make use ofwithRestApi's injected functions, and has its own props.
(In the actual app, withAuthentication also needs a react-router HOC, but am trying to keep things as simple as possible in this example.)
I've based my code so far on James Ravencroft's excellent post on HOCs and Typescript, and additionally this SO post on injected HOC props, which helped to solve an issue in which HOC props were being exposed to the parent of the wrapped component.
What I'm trying to achieve is:
- Access to
this.props.getResultOfApiCallfrom within theMyComponentclass, but hidden for any parent component. - Access to
this.props.isAuthenticatedfrom within theWithRestApiclass, but hidden for any parent component. - Ability to set
componentProponMyComponentfrom a parent component.
Code as follows:
MyBase.tsx, enclosing component purely to demonstrate use of MyComponent's prop:
import * as React from 'react';
import MyComponent from './MyComponent';
class MyBase extends React.Component {
public render() {
return (
<>
<h1>RESULT</h1>
<MyComponent componentProp={'Prop belonging to MyComponent'} />
</>
);
}
}
export default MyBase;
MyComponent.tsx, which uses the API:
import * as React from 'react';
import { IWithRestApiProps, withRestApi } from './WithRestApi';
interface IMyComponentProps extends IWithRestApiProps {
componentProp: string;
}
class MyComponent extends React.Component<IMyComponentProps> {
public render() {
return (
<>
<h2>Component prop: {this.props.componentProp}</h2>
<h2>API result: {this.props.getResultOfApiCall()}</h2>
</>
);
}
}
export default withRestApi(MyComponent);
WithAuthentication.tsx (putting this first because it's not the problem... as far as I can tell):
import * as React from 'react';
export interface IWithAuthenticationProps {
isAuthenticated: () => boolean;
}
export const withAuthentication = <P extends IWithAuthenticationProps>(Component: React.ComponentType<P>):
React.ComponentType<Pick<P, Exclude<keyof P, keyof IWithAuthenticationProps>>> =>
class WithAuthentication extends React.Component<P> {
public render() {
const { isAuthenticated, ...originalProps } = this.props as IWithAuthenticationProps;
return (
<Component
{...originalProps}
isAuthenticated={this.isAuthenticated}
/>
);
}
private readonly isAuthenticated = (): boolean => {
return true;
}
}
WithRestApi.tsx, which contains the typing problem.
import * as React from 'react';
import { IWithAuthenticationProps, withAuthentication } from './WithAuthentication';
export interface IWithRestApiProps extends IWithAuthenticationProps {
getResultOfApiCall: () => string;
}
export const withRestApi = <P extends IWithRestApiProps>(Component: React.ComponentType<P>):
React.ComponentType<Pick<P, Exclude<keyof P, keyof IWithRestApiProps>>> =>
withAuthentication(class WithRestApi extends React.Component<P> {
public render() {
const { getResultOfApiCall, ...originalProps } = this.props as IWithRestApiProps;
return (
<Component
{...originalProps}
getResultOfApiCall={this.getApiData}
/>
);
}
private readonly getApiData = () => {
if (this.props.isAuthenticated()) {
return 'Some API result';
} else {
return 'Not authenticated';
}
}
}) as React.ComponentType<P>; // TODO - remove this type assertion...?
This code builds, but as you can see I've had to type-assert the return value of the withApi HOC to React.ComponentType<P>. Without that assertion I see this Typescript error:
[ts]
Type 'ComponentType<Pick<P, Exclude<keyof P, "isAuthenticated">>>' is not assignable to type 'ComponentType<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>>'.
Type 'ComponentClass<Pick<P, Exclude<keyof P, "isAuthenticated">>, ComponentState>' is not assignable to type 'ComponentType<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>>'.
Type 'ComponentClass<Pick<P, Exclude<keyof P, "isAuthenticated">>, ComponentState>' is not assignable to type 'ComponentClass<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>, ComponentState>'.
Type 'Pick<P, Exclude<keyof P, "isAuthenticated">>' is not assignable to type 'Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>'.
Type 'Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">' is not assignable to type 'Exclude<keyof P, "isAuthenticated">'.
This is the first time I've run into a need for Pick..Exclude, so am a little hazy on exactly how the type is being matched in this case. I'm wondering if there's something about the way I'm using it here that could be improved to remove the need for the type assertion?