I am trying to figure out type annotation for higher order react components. Here's my code
class A extends React.Component<any, any> {
render() {
return <div>Hello World</div>
}
}
const HigherOrderComponent = (component:any) => {
return class extends component {
render() {
return super.render();
}
};
};
export default HigherOrderComponent(A);
typescript compiler throws following error
Error:(23, 24) TS2507: Type 'any' is not a constructor function type
I tried couple of annotation such as <any, any>, <T extends React.Component<any, any>>. None of them is passing through compilation.
See https://github.com/Microsoft/TypeScript/issues/5887 for an example of this. There is currently an issue with the approach outlined there but it is fixed in 1.8.0 nightly.
What you want is this:
const HigherOrderComponent = (component: typeof React.Component) => {
return class extends component<any, any> {
render() {
return super.render();
}
};
};
This worked and is very readable. Thanks Ryan.
@RyanCavanaugh Doesn't that lose typing on the returned class?
class Component<T, U> {
render() { }
}
const React = {
Component
}
const HigherOrderComponent = (component: typeof React.Component) => {
return class extends component<any, any> {
render() {
return super.render();
}
foo = 'bar';
};
};
const NewClass = HigherOrderComponent(React.Component);
const p = new NewClass();
p.foo; // string
@kitsonk I should have been more specific--doesn't it lose React Component props typing? (No one is instantiating React Components via new)
I think this is an issue for folks using JSX
The JSX tag is mapped to a constructor. It shouldn't lose its types, being instaniated programatically or by some JSX tag.
Well it does because the class being returned specifies the props to be type any:
return class extends component<any, any> {
If you put the correct types in here then the props should end up properly typed. However, having tried it, it does seem difficult to use the 'inheritance approach' with base components which are not generic. I was able to get it working using the 'composition approach' however:
/* higher-order component */
export interface HighlightedProps {
isHighlighted?: boolean;
}
export function Highlighted<T>(InputTemplate: React.ComponentClass<T>) {
return class extends React.Component<T & HighlightedProps> {
render() {
let className = this.props.isHighlighted ? "highlighted" : "";
return (
<div className={className}>
<InputTemplate {...this.props} />
</div>
);
}
}
}
/* some basic components */
interface MyInputProps {
inputValue: string;
}
class MyInput extends React.Component<MyInputProps, {}> { };
interface MyLinkProps {
linkAddress: string;
}
class MyLink extends React.Component<MyLinkProps, {}> { };
/* wrapped components */
const HighlightedInput = Highlighted(MyInput);
const HighlightedLink = Highlighted(MyLink);
/* usage example */
export class Form extends React.Component<{}, {}> {
render() {
return (
<div>
<HighlightedInput inputValue={"inputValue"} isHighlighted={false} />
<HighlightedLink linkAddress={"/home"} isHighlighted={true} />
</div>
);
}
}
@frankwallis That looks good, but for some reason I get Spead types may only be created from object types on {...this.props}, which seems to be tracked by this issue:
https://github.com/Microsoft/TypeScript/issues/10727
I haven't been able to figure out a workaround
this is how i've approached it until the new spread proposal goes live:
this wraps my routes entry component:
import * as React from 'react'
import connectState from 'hoc/connectState'
import { verifySocialSession, VerifySocialSession } from 'hoc/withAuth'
import { compose } from 'lib/helpers'
export interface InitializeProps {
initialized: boolean
}
interface InjectedProps extends VerifySocialSession {
storedSession: boolean | LastAction
}
interface State {
initialized: boolean
}
const initializeRoutesWrapper = <OP extends {}>(
WrappedComponent: React.SFC<OP & InitializeProps>
) => {
type Result = OP & InjectedProps
class InitializeRoutes extends React.Component<Result, State> {
constructor (props: Result) {
super(props)
this.state = {
initialized: false
}
}
public componentWillReceiveProps (next) {
const uninitialized = !this.state.initialized
const rehydratedSession = next.storedSession
if (uninitialized && rehydratedSession) {
this.verifySocial(next.storedSession.payload.session)
}
}
/**
* Use the token from the rehydrated state to login to the server with loginSocial.
* If there is an error authenticating, it will redirect to the redirectOnError param.
*/
public verifySocial = (session: Session) => {
if (session && session.sessionType === 'social') {
this.props.verifySocialSession(session.token)
.then(() => this.initialize())
}
}
public initialize = () => this.setState({ initialized: true })
public render () {
return <WrappedComponent initialized={true} {...this.props} />
}
}
const enhance = compose(
connectState(
(selectors: Selectors) => ({
storedSession: selectors.actionRehydrate
})
),
verifySocialSession({
redirectOnError: '/login'
})
)
return enhance(InitializeRoutes)
}
export default initializeRoutesWrapper
Here I'm wrapping my entry with a withSubRoutes HOC, which wraps the routes with a layout. I know that I will want to spread some props to my wrapped layout component, so I create a helper function to build the props that i will spread in the next HOC under the layout key. I create a type called LP since i'm not creating any static props within the component itself. If I were, i'd just extend them with whatever other interfaces i've imported.
import * as React from 'react'
import withSubRoutes, { RouteProps } from 'hoc/withSubRoutes'
import initialize, { InitializeProps } from './initialize'
import EntryLayout from './layout'
/** LayoutProps Shorthand */
type LP = InitializeProps
const AppEntry = withSubRoutes<LP>(EntryLayout)
const layout = (props: LP) => ({
...props
})
const Routes: React.SFC<RouteProps & LP> = ({ initialized, routes, store }) =>
<AppEntry layout={layout({ initialized })} routes={routes} store={store} />
export default initialize(Routes)
Then in my withSubRoutes, i'm able to spread the layout prop to the wrapped layout:
import * as React from 'react'
import NotFound from 'components/NotFound'
import { Route, Switch } from 'lib/router'
import { Store } from 'lib/types'
import { getDisplayName } from '../helpers'
export interface RouteProps {
routes: RouteConfig[],
store: Store<{}>
}
export interface SubRoutes<LayoutProps> extends RouteProps {
layout: LayoutProps,
}
/**
* LP = LayoutProps
*
* interface LP extends LayoutPropsInterfaceFromParent {
* staticLayoutProp: string
* }
*
* const MakeRoutes = withSubRoutes<LP>(Layout)
* const layout = (props: LP) => ({ ...props })
*
* const RenderRoutes = ({ propA, propB, propC }) => (
* <MakeRoutes propA={ propA } propB={ propB } layout={ layout({ propC, staticLayoutProp: 'red' }) } />
* )
*
*/
const composedMatchSubRoutes = <LP extends {}>(
WrappedComponent: React.SFC<LP>
) => {
const MatchRoutes: React.SFC<SubRoutes<LP>> = ({ routes, store, layout }) => {
return (
<WrappedComponent {...layout}>
<Switch>
{routes.map(({
routeComponent: RouteComponent,
routes: subRoutes,
...route
}) => (
<Route
key={route.id}
{...route}
children={({ ...routerProps }) => (
<RouteComponent {...routerProps} store={store} routes={subRoutes} />
)}
/>
))}
<Route path='*' render={({ location }) => <NotFound location={location} />} />
</Switch>
</WrappedComponent>
)
}
MatchRoutes.displayName = getDisplayName(WrappedComponent, 'subRoutes')
return MatchRoutes
}
export default composedMatchSubRoutes
It's certainly not perfect but it does work :)
Most helpful comment
What you want is this: