JLF
JLF

Reputation: 2360

Cannot use HOC property inside child component in Typescript

Problem

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;

-

Response to "Make 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

Answers (2)

Tyler Sebastian
Tyler Sebastian

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

cfraser
cfraser

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

Related Questions