Search code examples
javascriptreactjstypescriptstyled-components

How to hoist non react statics with typescript and styled-components?


I have a static three static properties (Header, Body, and Footer) set to a Dialog component. However, typescript throws the following error after wrapping the Dialog component in styled-components.

Property 'Header' does not exist on type 'StyledComponentClass...

Here is my /Dialog.tsx:

import { Dialog as BlueprintDialog, IDialogProps } from '@blueprintjs/core';
import * as React from 'react';
import styled from 'styled-components';

import Body from './Dialog.Body';
import Footer from './Dialog.Footer';
import Header from './Dialog.Header';

/** ************************************************************************* */

type DefaultProps = {
  className: string;
};

export interface DialogProps extends IDialogProps {
  children?: React.ReactNode;
  className?: string;
  primary?: boolean;
}

class Dialog extends React.PureComponent<DialogProps> {
  static displayName = 'UI.Dialog';
  static defaultProps: DefaultProps = {
    className: '',
  };
  static Body: typeof Body;
  static Footer: typeof Footer;
  static Header: typeof Header;
  render() {
    return <BlueprintDialog {...this.props} />;
  }
}

/** ************************************************************************* */

export default styled(Dialog)``;

And here is my index.ts where I piece it all together:

import Dialog from './Dialog';
import DialogBody from './Dialog.Body';
import DialogFooter from './Dialog.Footer';
import DialogHeader from './Dialog.Header';

Dialog.Body = DialogBody; // TS Compilation Error :/
Dialog.Footer = DialogFooter; // TS Compilation Error :/
Dialog.Header = DialogHeader; // TS Compilation Error :/
export default Dialog;

I've tried doing the following, which works, but now interpolation fails for the root Dialog component:

import { Dialog as BlueprintDialog, IDialogProps } from '@blueprintjs/core';
import * as React from 'react';
import styled from 'styled-components';

import Body from './Dialog.Body';
import Footer from './Dialog.Footer';
import Header from './Dialog.Header';

/** ************************************************************************* */

type DefaultProps = {
  className: string;
};

export interface DialogProps extends IDialogProps {
  children?: React.ReactNode;
  className?: string;
  primary?: boolean;
}

class Dialog extends React.PureComponent<DialogProps> {
  static displayName = 'UI.Dialog';
  static defaultProps: DefaultProps = {
    className: '',
  };
  render() {
    return <BlueprintDialog {...this.props} />;
  }
}

/** ************************************************************************* */

const Styled = styled(Dialog)``;

class WithSubmodules extends Styled {
  static Body: typeof Body;
  static Footer: typeof Footer;
  static Header: typeof Header;
}

export default WithSubmodules;

An example of interpolation that throws the Cannot call a class as a function error:

export default styled(InterpolationExample)`
  ${Dialog.Header} { /* WORKS :) */
    border: 1px solid green;
  }
  ${Dialog} { { /* Throws Error :/ */
    border: 1px solid pink;
  }
`;

Solution

  • So I did some tinkering and got something to work. If anybody has a better approach then please let me know!

    What I ended up doing is importing the StyledComponentClass from styled-components, then extending it with static properties in the following way:

    interface WithSubmodules extends StyledComponentClass<DialogProps, {}> {
      Body: typeof Body;
      Footer: typeof Footer;
      Header: typeof Header;
    }
    

    Next, I cast the returned StyledComponent to my new extended type, WithSubmodules:

    export default styled(Dialog)`` as WithSubmodules;
    

    Here is everything together:

    import { Dialog as BlueprintDialog, IDialogProps } from '@blueprintjs/core';
    import * as React from 'react';
    import styled, { StyledComponentClass } from 'styled-components';
    
    import Body from './Dialog.Body';
    import Footer from './Dialog.Footer';
    import Header from './Dialog.Header';
    
    /** ************************************************************************* */
    
    type DefaultProps = {
      className: string;
    };
    
    export interface DialogProps extends IDialogProps {
      children?: React.ReactNode;
      className?: string;
      primary?: boolean;
    }
    
    class Dialog extends React.PureComponent<DialogProps> {
      static displayName = 'UI.Dialog';
      static defaultProps: DefaultProps = {
        className: '',
      };
      render() {
        return <BlueprintDialog {...this.props} />;
      }
    }
    
    /** ************************************************************************* */
    
    interface WithSubmodules extends StyledComponentClass<DialogProps, {}> {
      Body: typeof Body;
      Footer: typeof Footer;
      Header: typeof Header;
    }
    
    export default styled(Dialog)`` as WithSubmodules;
    

    Again, if anyone has a better approach then let me know!

    Thanks