styled-components: [TypeScript] styled wrapper doesn't preserve generic props

TypeScript 2.9.1

Using the styled(Component) method while wrapping a component with generic props doesn’t preserve the generic argument, it simply defaults it to {}.

Reproduction

import React, { Fragment, ReactNode } from 'react';
import styled from 'styled-components';

interface Props<T> {
  renderChild: (props: T) => ReactNode;
  values: T[];
}

function GenericComponent<T>(props: Props<T>) {
  return (
    <Fragment>
      {props.values.map(props.renderChild)}
    </Fragment>
  );
}

const ThisWorks = (
  <GenericComponent
    values={[{ id: 1, value: 'foo' }]}
    renderChild={props => <p key={`I_${props.id}`}>{props.value}</p>}
  />
);

const StyledGenericComponent = styled(GenericComponent)`
  position: relative;
`;

const ThisDoesntWork = (
  <StyledGenericComponent
    values={[{ id: 1, value: 'foo' }]}
    renderChild={props => <p key={`I_${props.id}`}>{props.value}</p>} // Errors: Property 'id' does not exist on '{}', Property 'value' does not exist on '{}' 
  />
);

Expected Behavior

TypeScript should be able to evaluate the props object passed to renderChild, in this case, it should recognize it as id: number; value: string.

Actual Behavior

When wrapped in styled, props resolves to {}. renderChild in the wrapped example throws: Error: Property 'id' does not exist on '{}' Error: Property 'value' does not exist on '{}'

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 29
  • Comments: 35 (4 by maintainers)

Most upvoted comments

For anyone having this issue, this is my workaround:

const StyledSelect = styled(Select)`
  margin: 0 var(--spacing);
` as typeof Select;

This works perfectly for me:

const StyledComponent: new <T>() => Component<T> = styled(Component)`
    background-color: red;
` as any;

export default StyledComponent;

So far I’ve been using the following as a workaround:

const StyledGenericComponent = styled(GenericComponent)`
  position: relative;
`  as React.ComponentType as new <T>() => GenericComponent<T>;

const ThisWorksOnTs29 = <StyledGenericComponent<MyDataStructure>
  data={aMyDataStructureVar}
  />;

In case that the generic type is known upfront we can also use:

const StyledConcrenteComponent = styled(GenericComponent as new () => GenericComponent<MyDataStructure>)`
  position: relative;
`  as React.ComponentType as new <T>() => GenericComponent<T>;

const ThisAlsoWorksOnTs29 = <StyledConcrenteComponent
  data={aMyDataStructureVar}
  />;

which seems to be the casting equivalent of creating a concrete class for a given generic param. See: https://github.com/Microsoft/TypeScript/issues/3960

This works perfectly for me:

const StyledComponent: new <T>() => Component<T> = styled(Component)`
    background-color: red;
` as any;

export default StyledComponent;

I wonder why the any.

Anyway, for those with scoped generics, ya’ll could use this. ReactSlider is the name of your component.

const StyledReactSlider: new <T extends number | number[]>() => ReactSlider<T> = styled(ReactSlider)`
    background-color: red;
` as any;

Given StyledFoo = styled(Foo), another workaround is to write <StyledFoo<FC<FooProps<Bar>>> ... /> (instead of <StyledFoo<Bar>) to achieve same type safety as <Foo<Bar>.

See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/39136#issuecomment-719950054

For anyone having this issue, this is my workaround:

const StyledSelect = styled(Select)`
  margin: 0 var(--spacing);
` as typeof Select;

For a solution to support extra props passed for styled components CSS, I used

as <T>(props: SelectProps<T> & { $specialProp: string }) => React.ReactElement;

Does anyone see an issue with this approach?

I have found that I can’t pass transient props when using the above workaround. Any ideas?

@waynevanson @jgdev @acro5piano I’m confused how you guys are making that work.

Say I have a generic functional component like this:

export function MyComponent<Item>(
  props: React.PropsWithChildren<MyComponentProps<Item>>
): React.ReactElement | null {
  // ...
}

If I do the following, I get an error:

// => 'MyComponent' refers to a value, but is being used as a type here.
const MyComponentWithStyles: new <T>() => MyComponent<T> = styled(MyComponent)`
    background-color: red;
` as any;

This lines up with what @melounek experienced in https://github.com/DefinitelyTyped/DefinitelyTyped/issues/39136 . Not sure how you guys are able to instantiate things this way - am I missing something?

In the case of ReactNative FlatList, this is the solution I’m using, which seems like the simplest workaround:

type ItemType = {
  id: string;
  title: string;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const List: new () => FlatList<ItemType> = styled(FlatList)`` as any;

Although this does not add any of the of the styled-component props eg theme, css etc so far from ideal. Why is this ticket closed? Is there an associated ticket in the DefinatelyTyped repo?

As a workaround, I’m using the css property instead of a styled component, maybe that’s an option for someone else as well:

const styles = css`
  position: relative;
`;

const MyComp = () => {
  return (
    <MyGenericComponent<SomeType> css={styles} />
  );
};

For those of guys looking for a way to generic type of Ant Design component (my case is Ant Design Table), here is my solution:

import { Table, TableProps } from 'antd';

class WrapTableType<T> {
  wrapped(e: T) {
    return Table<TableProps<T>>(e);
  }
}

const MyTable = (styled(Table)`...` as React.ComponentType) as <T>(
  props: TableProps<T>,
) => ReturnType<WrapTableType<T>['wrapped']>;

Hello! I’ve found the next solution (without any and another hacks), maybe, it will help someone else. I’ve came to this view when I was trying to type my components from antd library, particularly its Select component. It can be generic. But below, I’ll put a common view of solution.

Definition:

// <Generic /> - it's a Select from antd library, for example, in another words: styled generic

export function StyledGeneric<T>(): StyledComponent<React.FC<GenericProps<T>>, {}, {}, never> {
    return styled(props => <Generic<T> {...props} />)`
        // ... some styles
    `;
}

Use-case

// ...other imports
import { StyledGeneric } from './StyledGeneric';

const GenericBase = <T extends {}>({
   // ...some props
}: GenericWrapperProps<T>) => {
    const handleChange = (selection: T, option: React.ReactElement | React.ReactElement[]) => {
         // ...some handler logic
    };
    const TypedSelect = StyledGeneric<T>();
    return (
        <TypedSelect onChange={handleChange}>
            {/* options mapping */}
        </TypedSelect>
    );
};

Update#1 CAUTION!!! The implementation above has a some performance issue (thanks to @dfernandez-asapp) with creating of new styled for each render, you should just define const TypedSelect = StyledGeneric<T>(); outside of render method and where you already know an exact type (you must have this place in any case somewhere in app).

Update#2 Updated version here: https://github.com/styled-components/styled-components/issues/1803#issuecomment-665752971

Hey folks, TS typings for styled-components have been moved to DefinitelyTyped, so please feel free to move your issue there where you’ll receive better community support.

any news on this? it is a blocker for us

Hi all, I have a solution that worked for me (and doesn’t involve type suggestions to any). See the full code below.

It would be nice if this could be done a) without a type suggestion, and b) retaining the generic after the styled invocation, though I don’t think generics can survive through TypeScript’s inference passing through a function call like that, so these might be infeasible at the moment. so I think a type suggestion will be as good as it gets for now.

interface ExampleProps<Generic> {
  genericMember: Generic;
}
type ExampleComponent<Generic> = React.FC<ExampleProps<Generic>>;

// functions can't "implement" a type as far as I know, so we do it by setting the
// proper values for parameters and return type
function Example<Generic>(
  ...[props]: Parameters<ExampleComponent<Generic>>
): ReturnType<ExampleComponent<Generic>> {
  return <p>{props.genericMember}</p>;
}

// now let's say you want the component, with a string as the generic. If you leave out
// the type suggestion, you get ExampleComponent<unknown> instead:
const StyledExample = styled(Example as ExampleComponent<string>)`
  color: red;
`;
const Page: React.FC = () => <StyledExample genericMember="value" />; // works
const OtherPage: React.FC = () => <StyledExample genericMember={5} />; // fails

As an alternative, you can also use the Factory pattern to achieve the same thing without a type suggestion:

function makeExample<Generic>(): ExampleComponent<Generic> {
  return Example;
}
// if you leave out the type parameter, you get `ExampleComponent<unknown>` instead:
const styledExample = styled(makeExample<number>())`
  color: blue;
`;

Hope this helps someone!

For anyone having this issue, this is my workaround:

const StyledSelect = styled(Select)`
  margin: 0 var(--spacing);
` as typeof Select;

For a solution to support extra props passed for styled components CSS, I used

as <T>(props: SelectProps<T> & { $specialProp: string }) => React.ReactElement;

Does anyone see an issue with this approach?

Yes. It will miss out on the execution props like “as”, “forwardedAs” and “theme”.

Like some of you, I’m using antd and this is the solution I came up with:

const styledTable = <RecordType extends object = any>(T: typeof Table) => styled((props: TableProps<RecordType> & { className?: string }) => {
    return <T {...props}/>
})`
  ...
`

const MyTable = styledTable<RecordType>(Table/* coming from antd*/)

It seems that if you don’t need to use the props in the styled template you’re ok with some of the above methods.

However, if you want to use the props in the css you have to make an intermediary React Component that forwards the generic type and then wrap that in styled() as so

interface StyledRadioGroupProps<T> extends RadioGroupProps<T> {
  horizontal?: boolean
}

const ExtendedRadioGroup = <T,>(props: StyledRadioGroupProps<T>): ReactElement => (
  <MuiRadioGroup {...props} />
)
const StyledRadioGroup = styled(ExtendedRadioGroup)`
  flex: 1;
  flex-direction: ${(props) => (props.horizontal ? 'row' : 'column')};
  gap: 1rem;
`

@badsyntax nope, emotion doesn’t work either:

const X = function<T>(props: { wow: T, hey: T}) { return  null }
const A = styled(X)<{ $extra: 'yes' }>`color: red;`

then using it like this:

<A wow={123} hey='asd' $extra='yes' />

it compiles perfectly (should fail) and shows the following types (notice the unknown as the prop type instead of it being a generic component):

const A: StyledComponent<{
    wow: unknown;
    hey: unknown;
} & {
    theme?: Theme | undefined;
} & {
    $extra: 'yes';
}, {}, {}>

in reality it should fail since both props should be of the same type T

For anyone having this issue, this is my workaround:

const StyledSelect = styled(Select)`
  margin: 0 var(--spacing);
` as typeof Select;

This works for most use-cases, but StyledSelect now cannot be referred to in another styled component definition:

const Select: FC<void> = () => <div />

const StyledSelect = styled(Select)`
  margin: 0 var(--spacing);
` as typeof Select

const SelectWrapper = styled.div`
  &:hover ${StyledSelect} {
    // ...
}
Results in this error:
TS2769: No overload matches this call.   Overload 1 of 3, '(first: TemplateStringsArray | CSSObject | InterpolationFunction<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<...>> & { ...; }, any>>, ...rest: Interpolation<...>[]): StyledComponent<...>', gave the following error.     Argument of type 'FC<void>' is not assignable to parameter of type 'Interpolation<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; }, any>>'.       Type 'FunctionComponent<void>' is not assignable to type 'InterpolationFunction<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; }, any>>'.         Types of parameters 'props' and 'props' are incompatible.           Type 'ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; }, any>' is not assignable to type 'PropsWithChildren<void>'.             Type 'ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; }, any>' is not assignable to type 'void'.   Overload 2 of 3, '(first: TemplateStringsArray | CSSObject | InterpolationFunction<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<...>> & { ...; } & void & { ...; }, any>>, ...rest: Interpolation<...>[]): StyledComponent<...>', gave the following error.     Argument of type 'FC<void>' is not assignable to parameter of type 'Interpolation<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; } & void & { ...; }, any>>'.       Type 'FunctionComponent<void>' is not assignable to type 'InterpolationFunction<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; } & void & { ...; }, any>>'.         Type 'ReactElement<any, any> | null' is not assignable to type 'Interpolation<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; } & void & { ...; }, any>>'.           Type 'ReactElement<any, any>' is not assignable to type 'Interpolation<ThemedStyledProps<Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<HTMLDivElement>> & { ...; } & void & { ...; }, any>>'.             Type 'ReactElement<any, any>' is not assignable to type 'CSSObject'.               Index signature for type 'string' is missing in type 'ReactElement<any, any>'.

I don’t think these PRs are gonna fix this issue. The problem here is here: const StyledGenericComponent = styled(GenericComponent). With this, styled does not expect a generic component, it expects a concrete component (with concrete prop type). In general, this is almost impossible to distinguish a generic component from a regular one when it comes to higher-order functions. I can think of how it can be worked around, but you can read this TS issue where such problems are discussed: https://github.com/Microsoft/TypeScript/issues/9366