How to get StyledComponent's type in intellisense?

Viewed 46

I have created a function which takes a component and returns a Styled component of that

import styled from 'styled-components';

const withStyled = (Component, styles) => {
  const StyledComponent = styled(Component)`
    ${styles}
  `;

  return StyledComponent;
};

export default withStyled;

I am using this withStyled function as below

import cx from 'classnames';
import React from 'react';
import withStyled from '../../../hoc/withStyled';
import styles from './Paragraph.style';

interface Props {
  className: string;
  sizeVariant: string;
  children: string;
}

const Paragraph = (props: Props) => {
  const { children, className, ...extraProps } = props;
  return (
    <p className={cx('paragraph', className)} {...extraProps}>
      {children}
    </p>
  );
};

const StyledParagraph = withStyled(Paragraph, styles);

export default StyledParagraph;

But when I am using my Paragraph component, VS code intellisense is not showing me suggestions related to props This is because withStyled function is returning me a new component and my original component's types are lost in the process.

Is there a way to get the original types in StyledComponent too?

2 Answers

TLDR

function withStyled<
  T extends keyof JSX.IntrinsicElements | React.ComponentType<any>
>(component: T, styles: string): T {
  return styled(component)`${styles}`;
}

Stackblitz: https://stackblitz.com/edit/react-ts-rbqxgt?file=App.tsx


The @types/styled-components package seems to be a bit busted, since the StyledComponent type just resolves to any. This is what I get from Intellisense:

type StyledComponent<
  C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
  T extends object,
  O extends object = {},
  A extends string | number | symbol = never
> = any;

You can use generics to just return the same component type. FC stands for Function Component.

function withStyled<T>(component: React.FC<T>, styles: string): React.FC<T> {
  return styled(component)`${styles}`;
}

An arrow function requires you to write <T extends Object> or <T,_> instead of just <T> else it clashes with JSX.

const withStyled = <T extends Object>(
  Component: React.FC<T>,
  styles: string
): React.FC<T> => styled(Component)`${styles}`;

Then, this line:

const StyledParagraph = withStyled(Paragraph, styles);

Produces this intellisense:

const StyledParagraph: React.FC<Props>

And you will get type errors as normal.


To allow for passing any acceptable types, not just function components, you can use T extends keyof JSX.IntrinsicElements | React.ComponentType<any> and just return the same type, as shown in the TLDR.

withStyled function is returning me a new component and my original component's types are lost in the process.

Because the Component 1st argument of the withStyled function is not typed, TypeScript infers it as any. This is an error in TS with strict mode.

Therefore, styled(Component) is also inferred as being applied on an "any" component, with no information about its props:

const withStyled = (Component, styles) => { // Parameter 'Component' implicitly has an 'any' type. (same for 'styles')
  //                ^? (parameter) Component: any
  const StyledComponent = styled(Component)`
    ${styles}
  `;

  return StyledComponent;
  //     ^? StyledComponent<any, any, object, string | number | symbol>
};

Similarly to @ChrisHamilton's answer, we can use a constrained generic type to let TS know what is the Component argument type. However, there is no need to assert the function returned type, TS is able to infer it from the styled function. That way, you get the actual styled component type, in particular with the extra props added by styled-components library, like as, forwardedAs and theme:

function withStyled2<T extends Parameters<typeof styled>[0]>(Component: T, styles: string) {
  //     ^? function withStyled2<T extends keyof JSX.IntrinsicElements | React.ComponentType<any>>(Component: T, styles: string): StyledComponent<T, any, {}, never>
  const StyledComponent = styled(Component)`
    ${styles}
  `;

  return StyledComponent;
  //     ^? StyledComponent<T, any, {}, never>
};

const StyledParagraph2 = withStyled2(Paragraph, styles);
//    ^? StyledComponent<(props: Props) => JSX.Element, any, {}, never>

and the styled component props are correctly checked:

function ConsumerComponent2() {
  return (
    <StyledParagraph2
      className="foo"
      sizeVariant="bar"
      unexpectedProp="should error" // Property 'unexpectedProp' does not exist on type 'IntrinsicAttributes & { className: string; children: string; sizeVariant: string; } & { theme?: any; } & { as?: ((props: Props) => Element) | undefined; forwardedAs?: ((props: Props) => Element) | undefined; }'.
    >
      Hello
    </StyledParagraph2>
  )
}

Playground Link

Related