Definitelytyped: [@types/styled-components] Generics in functional components (and best practise for generics in general)

Created on 15 Oct 2019  路  10Comments  路  Source: DefinitelyTyped/DefinitelyTyped

I'm trying to use style my functional components with generics, but cannot find any approach to reach that.

For class-components is working this workaround: https://github.com/styled-components/styled-components/issues/1803#issuecomment-407332173

but for functional one, I didn't find a way how to do that, like you can see in the example below.

import React from 'react';
import styled from 'styled-components';

let MyComponent: any;

type Props<T> = { a: T };

// with Functional doesn't work
const FunctionalComponent: <T>(p: Props<T>) => React.ReactElement<Props<T>> = props => (
  <MyComponent {...props} />
);
const StyledFunctional = styled(FunctionalComponent)`
  color: red;
`;
const StyledFunctionalRetyped = (styled(FunctionalComponent)`
  color: red;
` as React.ReactNode) as new <T>() => FunctionalComponent<T>;
//                                    -------------------
// => 'FunctionalComponent' refers to a value, but is being used as a type here.

// with Class component works fine
class ClassComponent<T> extends React.Component<Props<T>> {
  render() {
    return <MyComponent {...this.props} />;
  }
}
const StyledClassRetyped = (styled(ClassComponent)`
  color: red;
` as React.ReactNode) as new <T>() => ClassComponent<T>;

// example of usage
const Examples = (
  <>
    <StyledFunctional<string> a={8} />
    {/*               ------ => doesn't support generics (expected 0 arguments) */}
    <StyledFunctionalRetyped<string> a={8} /> {/* doesn't work*/}
    <StyledClassRetyped<string> a={8} /> {/* works properly (shows error) */}
  </>
);

  • [x] I tried using the @types/styled-components package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] Mention the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @Igorbek @Igmat @Lavoaster @Jessidhia @JKillian @eps1lon @flavordaaave @wagerfield

Most helpful comment

Thanks @Naararouter !

Did some cleaning (e.g. removing StyledComponent) to find out core of the issue and it's all about wrapping the styled() component by a generic one, which handles the generics-typing.

import React from "react";
import styled from "styled-components";

type Props<T> = { a: T; className?: string; }

const GenericComponent: <T>(p: Props<T>) => 
    React.ReactElement<Props<T>> = ({ a, ...props }) => <div {...props}>{a}</div>;

// wrapped styled-component and re-typed it works as expected
const StyledGeneric = <T extends {}>(props: Props<T>) => {
  const StyledComponent = styled<React.FC<Props<T>>>(GenericComponent)`
    color: red;
  `;
  return <StyledComponent {...props} />;
};

export const Example = () => <StyledGeneric<number> a={8} />;

Closing.

All 10 comments

I am having the exact same issue. Is there any way to work around this and preserve type safety?

Even being able to convert the functional component into a concrete implementation and then passing it to styled like styled(FunctionalComponent<string>) would be super useful.

same!

I've left my current workaround at the same issue on the styled-components repo: https://github.com/styled-components/styled-components/issues/1803#issuecomment-550895376

Maybe there's some TypeScript feature I don't know about, but I suspect that TypeScript itself isn't expressive enough to preserve the generics as is. After looking at the styled-components code it seems that the new type for the resulting component gets new props via this code in styled-base/types/helper:

export type PropsOf<
  Tag extends React.ComponentType<any>
> = Tag extends React.SFC<infer Props>
  ? Props & React.Attributes
  : Tag extends React.ComponentClass<infer Props>
    ? (Tag extends new (...args: Array<any>) => infer Instance
        ? Props & React.ClassAttributes<Instance>
        : never)
    : never

So in order to calculate the new props, it has to evaluate the original ones, and when it tries to do that I believe it just assumes the generics to be as general as possible given their constraints.

If TypeScript were to allow it to remain parameterized, then for each use of an unspecified generic, TypeScript would need to include that generic in the resulting type (which there can be arbitrary amounts depending on how many generic type parameters you pass in to the parent's generics); it would probably also preclude the parent from explicitly specifying its generics, because if it did then it wouldn't be correct if any of the type parameters passed in were generic.

I think this might be related to this long-unresolved issue on the TypeScript repo: https://github.com/microsoft/TypeScript/issues/1213

I've found the next solution for me. It seems, it fulfils a need task: https://github.com/styled-components/styled-components/issues/1803#issuecomment-573196034

Update#1:
Adapted the message above for resolving of author issue exact, in one file:

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

interface Props<T> {
    a: T;
}

const MyComponent: <T>(p: Props<T>) => React.ReactElement<Props<T>> = ({ a }) => <div>{a}</div>;

const FunctionalComponent: <T>(p: Props<T>) => React.ReactElement<Props<T>> = props => <MyComponent {...props} />;

function StyledFunctional<T>(): StyledComponent<React.FC<Props<T>>, {}, {}, never> {
    return styled(props => <FunctionalComponent<T> {...props} />)`
        color: red;
    `;
}

const GenericBase = <T extends {}>({ a }: Props<T>) => {
    const TypedStyledFunctional = StyledFunctional<T>();
    return <TypedStyledFunctional a={a} />;
};

const Examples = () => {
    return (
        <>
            <GenericBase<number> a={8} />
        </>
    );
};

Thanks @Naararouter !

Did some cleaning (e.g. removing StyledComponent) to find out core of the issue and it's all about wrapping the styled() component by a generic one, which handles the generics-typing.

import React from "react";
import styled from "styled-components";

type Props<T> = { a: T; className?: string; }

const GenericComponent: <T>(p: Props<T>) => 
    React.ReactElement<Props<T>> = ({ a, ...props }) => <div {...props}>{a}</div>;

// wrapped styled-component and re-typed it works as expected
const StyledGeneric = <T extends {}>(props: Props<T>) => {
  const StyledComponent = styled<React.FC<Props<T>>>(GenericComponent)`
    color: red;
  `;
  return <StyledComponent {...props} />;
};

export const Example = () => <StyledGeneric<number> a={8} />;

Closing.

@melounek I'm glad that my way was helpful, but...be careful, your simplification has, at least, one critical side-effect, that's why I came to my way exact.
It will broken if you add, for example, onChange?: (value: T) => void; in your interface Prop<T>. Pretty common case.

Thanks! -> I updated my simplified solution replacing styled() by styled<React.FC<Props<T>>>() so it handles your use-case too.

@melounek Still doesn't work for my initial case with antd library :( so...I'll be glad if you will have a time to let me know where can I miss something by adapting the code below for your example. I had been trying and...Did not work out, different unpleasant type error on each steps :'(

Source:
(this works fine, but adaptation by your method didn't work out)

import { Select } from 'antd';
import { SelectProps } from 'antd/lib/select';
import React from 'react';
import styled, { StyledComponent } from 'styled-components';

export function StyledSelect<T>(): StyledComponent<React.FC<SelectProps<T>>, {}, {}, never> {
    return styled(props => <Select<T> {...props} />)`
        &.ant-select {
            width: 100%;
        }
    `;
}

P.s.: to be honest, maybe, this is related with antd definition mostly. I still have not had a time to check it thoroughly, but...in any case, styled-components & typescript is here...and I think we can continue this conversation here yet :D

@Naararouter I would do it this way:

const StyledSelect = <T extends {}>(props: SelectProps<T>) => {
  const StyledComponent = styled<React.FC<SelectProps<T>>>(
    (Select as unknown) as React.FC<SelectProps<T>>
  )`
    color: red;
  `;
  return <StyledComponent {...props} />;
};

But I think, your solution is also OK.
You are re-typing styled-component internals and I am re-typing antd/select internals, so neither one is perfect, but both will do it's job, so it's good to have both our codes in this thread.

PS: Another option is to wrap the antd/select with styled

to workaround this.

The generic argument for the wrapped component can be passed like <StyledFoo<FC<Props<Bar>>> ... /> to provide the same type safty as <Foo<Bar> ... /> does.

Base on the example provided by OP:

鍦栫墖

Now ts can tell that '8' is valid while 8 is invalid

P.s. I also use a similar workaround for antd/select

鍦栫墖

ValueType=number

Environment:

  • styled-components: 5.0.0
  • @types/styled-components: ^5.1.4
Was this page helpful?
0 / 5 - 0 ratings