Reputation: 2360
I've wrapped a component with a typical HOC that I've used in some format on other non-TS projects. My problem is that I can't use the HOC's prop currentBreakpoint
inside of my wrapped component because it expects it to be in the component's type:
Property 'currentBreakpoint' does not exist on type 'Readonly<OwnProps> & Readonly<{ children?: ReactNode; }>'. TS2339
So then I import the HOC's props interface and merge it into my component's own prop interface but then I get:
Property 'currentBreakpoint' is missing in type '{}' but required in type 'Readonly<Props>'. TS2741
As it expects the prop currentBreakpoint
to be defined when I invoke <ChildComponent />
in my view – even though this is provided by the HOC.
-
Here are my files:
ChildComponent.js
import React, { Component } from 'react';
import withBreakpoints, {
Breakpoints,
Props as WithBreakpointsProps
} from 'lib/withBreakpoints';
interface OwnProps {}
type Props = WithBreakpointsProps & OwnProps;
class ChildComponent extends Component<Props> {
constructor(props: Props) {
super(props);
this.state = {
daysPerPage: this.getDaysPerPageFromViewportSize(),
cursor: new Date()
};
}
getDaysPerPageFromViewportSize = () => {
// the problem area
const { currentBreakpoint } = this.props;
let daysPerPage;
switch (currentBreakpoint) {
case Breakpoints.SMALL.label:
case Breakpoints.EXTRA_SMALL.label:
daysPerPage = 1;
break;
case Breakpoints.MEDIUM.label:
case Breakpoints.LARGE.label:
daysPerPage = 4;
break;
case Breakpoints.EXTRA_LARGE.label:
daysPerPage = 6;
break;
default:
daysPerPage = 1;
break;
}
return daysPerPage
};
render() {
return (
<div className="AvailabilityCalendar" />
);
}
}
export default withBreakpoints<Props>(AvailabilityCalendar);
withBreakpoints.ts
import React, { Component, ComponentType } from 'react';
export type CurrentBreakpoint = string | null;
export interface Props {
currentBreakpoint: string
}
export interface Breakpoint {
label: string;
lowerBound: number;
upperBound: number;
}
export interface State {
currentBreakpoint: CurrentBreakpoint;
}
export const Breakpoints: {
[id: string]: Breakpoint
} = {
EXTRA_SMALL: {
label: 'EXTRA_SMALL',
lowerBound: 0,
upperBound: 640
},
SMALL: {
label: 'SMALL',
lowerBound: 641,
upperBound: 1024
},
MEDIUM: {
label: 'MEDIUM',
lowerBound: 1025,
upperBound: 1280
},
LARGE: {
label: 'LARGE',
lowerBound: 1281,
upperBound: 1920
},
EXTRA_LARGE: {
label: 'EXTRA_LARGE',
lowerBound: 1921,
upperBound: 1000000
}
};
const withBreakpoints = <WrappedComponentProps extends object>(
WrappedComponent: ComponentType<WrappedComponentProps>
) => {
class WithBreakpoints extends Component<WrappedComponentProps, State> {
constructor(props: WrappedComponentProps) {
super(props);
this.state = {
currentBreakpoint: this.getCurrentBreakpoint()
};
}
componentDidMount() {
window.addEventListener('resize', this.checkBreakpoints);
}
componentWillUnmount() {
window.removeEventListener('resize', this.checkBreakpoints);
}
checkBreakpoints = () => {
let currentBreakpoint: CurrentBreakpoint = this.getCurrentBreakpoint();
if (currentBreakpoint !== this.state.currentBreakpoint) {
this.setState({ currentBreakpoint });
}
};
getCurrentBreakpoint = (): CurrentBreakpoint => {
const currentViewportWidth: number = Math.round(window.innerWidth);
return Object.keys(Breakpoints).find(
key =>
Breakpoints[key].lowerBound < currentViewportWidth &&
Breakpoints[key].upperBound > currentViewportWidth
) || null;
};
render() {
return (
<WrappedComponent
{...this.props as WrappedComponentProps}
currentBreakpoint={this.state.currentBreakpoint}
/>
);
}
}
return WithBreakpoints;
};
export default withBreakpoints;
-
currentBreakpoint
optional on the ChildComponent
".I have seen this as the accepted answer on other questions but I think we can all agree this is an inappropriate use of the optional flag on properties and defeats the purpose of using Typescript.
Upvotes: 1
Views: 1582
Reputation: 9448
To expand on @cfraser's answer, a common pattern for HoCs which inject internal props is to type your HoC
// these will be injected by the HoC
interface SomeInternalProps {
someProp: any
}
function hoc<P>(Wrapped: React.Component<P & SomeInternalProps>): React.Component<Omit<P, keyof SomeInternalProps>>
i.e. hoc
expects a component that takes props P
AND SomeInternalProps
and returns a component that only expects P
.
You can even omit the generic argument and TS will figure it out. e.g.
function SomeComponent(props: { prop: any } & SomeInternalProps) {...}
export default hoc(SomeComponent) // Component<{ prop: any }>
See here for info on Omit
Upvotes: 2
Reputation: 961
You should read about ts generics.
For example, let's say a component has external props like type OwnProps = { a: string }
, and the HOC injects type InjectedProps = { b: boolean }
. This means the final props for the component will be type Props = OwnProps & InjectedProps
.
But, if you do:
const Component = (props: Props) => {}
export default hoc(Component);
and try to use that component only passing the a
prop, it will complain about not receiving the b
prop, when that prop is actually received internally.
What you can do is something like:
const Component = (props: Props) => {}
export default hoc<OwnProps>(Component);
So that the hoc'd component knows what props it needs externally, and which ones are internally received.
Having that, it should take some tweaking with the HOC definition and the generic it receives to have the proper final Props
.
Upvotes: 1