Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript support for extended Styled Components #38

Closed
damien-mcmahon opened this issue Apr 15, 2020 · 11 comments
Closed

Typescript support for extended Styled Components #38

damien-mcmahon opened this issue Apr 15, 2020 · 11 comments

Comments

@damien-mcmahon
Copy link

Hi,
I've been using the macro to extend components and have recently started also using Typescript and I'm getting errors from the tsc when using extended components:

So, this works in regular JSX:

const PinkButton = tw(Button)`bg-pink`;

but when it becomes TSX then I get this error:

Argument of type 'FunctionComponent<Props>' is not assignable to parameter of type 'TemplateStringsArray'.

The workaround for this is to import styled and call it like this:

const PinkButton = styled(Button)`${tw`bg-pink`}`;

Which is great for now but everywhere else I'm using tw.div etc and would like to be able to consistently use tw instead of having to import styled when extending components.

#24 fixed other issues but there's still this case.

@rbutera
Copy link
Contributor

rbutera commented Apr 15, 2020

attn: @kingdaro

@itsMapleLeaf
Copy link
Contributor

I don't really feel personally motivated to implement this. Firstly because it's not documented(?), and ideologically, I feel like using this a lot would lead down an inheritance-heavy path over that favoring composition. An example in case I'm unclear:

// these are only a few classes, but imagine they're way longer, since buttons generally are
const Button = tw.button`...`
const PinkButton = tw(Button)`bg-pink`
const UnderlineButton = tw(Button)`underline`
const PinkUnderlineButton = tw(/* ??? */)` ??? `

// I'd prefer this
const pinkButtonStyle = `bg-pink`
const underlineButtonStyle = `underline`

// then apply them to any element/component
// don't have to make new styled components, or fool around with 'as' prop
<button css={pinkButtonStyle} />
<a css={underlineButtonStyle} />
<Link to="/" css={[pinkButtonStyle, underlineButtonStyle]} />

There are ways around the diamond problem I've pointed out, but with the alternate path here, it's something I don't even have to think about, and I think it aligns more with Tailwind's core philosophy.


That aside, if someone wants to pick up the mantle, I'll gladly review the PR, provided the type tests are also updated

@ben-rogerson
Copy link
Owner

ben-rogerson commented Apr 17, 2020

Thanks for your thoughts on this Daro.
I agree with your Tailwind alignment points and thanks for alerting me to the lack of documentation on the component wrapping.
I use component wrapping on the regular and actually don't go past a single level 😮
I'm planning to create some TypeScript demos in the future and it would be great to add typescript support for the feature without the workaround.

Edit: I've now documented the wrapping in feature in the readme.

@aaronbski
Copy link

+1

@ben-rogerson
Copy link
Owner

I'm keen on adding this but not sure where to start.
Would someone like to submit a PR?

@rbutera
Copy link
Contributor

rbutera commented May 22, 2020

It appears that emotion has an issue related to this that will be fixed in v11: emotion-js/emotion#1823

Perhaps best to look at how v11 handles the types:

// Definitions by: Junyoung Clare Jang <https://github.com/Ailrun>
// TypeScript Version: 3.2

import * as React from 'react'
import { ComponentSelector, Interpolation } from '@emotion/serialize'
import { PropsOf, DistributiveOmit, Theme } from '@emotion/react'

export {
  ArrayInterpolation,
  CSSObject,
  FunctionInterpolation
} from '@emotion/serialize'

export { ComponentSelector, Interpolation }

/** Same as StyledOptions but shouldForwardProp must be a type guard */
export interface FilteringStyledOptions<
  Props,
  ForwardedProps extends keyof Props = keyof Props
> {
  label?: string
  shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps
  target?: string
}

export interface StyledOptions<Props> {
  label?: string
  shouldForwardProp?(propName: PropertyKey): boolean
  target?: string
}

/**
 * @typeparam ComponentProps  Props which will be included when withComponent is called
 * @typeparam SpecificComponentProps  Props which will *not* be included when withComponent is called
 */
export interface StyledComponent<
  ComponentProps extends {},
  SpecificComponentProps extends {} = {}
> extends React.FC<ComponentProps & SpecificComponentProps>, ComponentSelector {
  withComponent<C extends React.ComponentType<React.ComponentProps<C>>>(
    component: C
  ): StyledComponent<ComponentProps & PropsOf<C>>
  withComponent<Tag extends keyof JSX.IntrinsicElements>(
    tag: Tag
  ): StyledComponent<ComponentProps, JSX.IntrinsicElements[Tag]>
}

/**
 * @typeparam ComponentProps  Props which will be included when withComponent is called
 * @typeparam SpecificComponentProps  Props which will *not* be included when withComponent is called
 */
export interface CreateStyledComponent<
  ComponentProps extends {},
  SpecificComponentProps extends {} = {}
> {
  /**
   * @typeparam AdditionalProps  Additional props to add to your styled component
   */
  <AdditionalProps extends {} = {}>(
    ...styles: Array<
      Interpolation<
        ComponentProps &
          SpecificComponentProps &
          AdditionalProps & { theme: Theme }
      >
    >
  ): StyledComponent<ComponentProps & AdditionalProps, SpecificComponentProps>

  (
    template: TemplateStringsArray,
    ...styles: Array<
      Interpolation<ComponentProps & SpecificComponentProps & { theme: Theme }>
    >
  ): StyledComponent<ComponentProps, SpecificComponentProps>

  /**
   * @typeparam AdditionalProps  Additional props to add to your styled component
   */
  <AdditionalProps extends {}>(
    template: TemplateStringsArray,
    ...styles: Array<
      Interpolation<
        ComponentProps &
          SpecificComponentProps &
          AdditionalProps & { theme: Theme }
      >
    >
  ): StyledComponent<ComponentProps & AdditionalProps, SpecificComponentProps>
}

/**
 * @desc
 * This function accepts a React component or tag ('div', 'a' etc).
 *
 * @example styled(MyComponent)({ width: 100 })
 * @example styled(MyComponent)(myComponentProps => ({ width: myComponentProps.width })
 * @example styled('div')({ width: 100 })
 * @example styled('div')<Props>(props => ({ width: props.width })
 */
export interface CreateStyled {
  <
    C extends React.ComponentType<React.ComponentProps<C>>,
    ForwardedProps extends keyof React.ComponentProps<
      C
    > = keyof React.ComponentProps<C>
  >(
    component: C,
    options: FilteringStyledOptions<PropsOf<C>, ForwardedProps>
  ): CreateStyledComponent<Pick<PropsOf<C>, ForwardedProps> & { theme?: Theme }>

  <C extends React.ComponentType<React.ComponentProps<C>>>(
    component: C,
    options?: StyledOptions<PropsOf<C>>
  ): CreateStyledComponent<PropsOf<C> & { theme?: Theme }>

  <
    Tag extends keyof JSX.IntrinsicElements,
    ForwardedProps extends keyof JSX.IntrinsicElements[Tag] = keyof JSX.IntrinsicElements[Tag]
  >(
    tag: Tag,
    options: FilteringStyledOptions<JSX.IntrinsicElements[Tag], ForwardedProps>
  ): CreateStyledComponent<
    { theme?: Theme },
    Pick<JSX.IntrinsicElements[Tag], ForwardedProps>
  >

  <Tag extends keyof JSX.IntrinsicElements>(
    tag: Tag,
    options?: StyledOptions<JSX.IntrinsicElements[Tag]>
  ): CreateStyledComponent<{ theme?: Theme }, JSX.IntrinsicElements[Tag]>
}

declare const styled: CreateStyled
export default styled

@A-Shleifman
Copy link

A-Shleifman commented Jul 9, 2020

As a temporary solution we can declare a modified module:

// twin.d.ts

import { ComponentType } from 'react';
import { TwFn, TemplateFn } from 'twin.macro';

declare module 'twin.macro' {
  type TwComponentWrapper = <T extends ComponentType<any>>(component: T) => TemplateFn<T>;
  const tw: TwFn & TwComponentMap & TwComponentWrapper;
  export = tw;
}

@ben-rogerson, how about updating the type with this?

@ben-rogerson
Copy link
Owner

Thanks for looking into this - it all looks good at first glance. I'll aim to release a patch after some testing.

@Vinlock
Copy link

Vinlock commented Jul 17, 2020

As a temporary solution we can declare a modified module:

// twin.d.ts

import { ComponentType } from 'react';
import { TwFn, TemplateFn } from 'twin.macro';

declare module 'twin.macro' {
  type TwComponentWrapper = <T extends ComponentType<any>>(component: T) => TemplateFn<T>;
  const tw: TwFn & TwComponentMap & TwComponentWrapper;
  export = tw;
}

@ben-rogerson, how about updating the type with this?

Works great for me so far! Thanks!

@ben-rogerson
Copy link
Owner

I've put up a PR if anyone wants to take a squiz before I merge.

@ben-rogerson
Copy link
Owner

ben-rogerson commented Jul 22, 2020

Update to 1.6.0 and all should be good now 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants