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

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

Closed
4 tasks done
melounek opened this issue Oct 15, 2019 · 25 comments

Comments

@melounek
Copy link

melounek commented Oct 15, 2019

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: styled-components/styled-components#1803 (comment)

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) */}
  </>
);
@kyrim
Copy link

kyrim commented Oct 21, 2019

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.

@osdiab
Copy link
Contributor

osdiab commented Nov 6, 2019

same!

@osdiab
Copy link
Contributor

osdiab commented Nov 7, 2019

I've left my current workaround at the same issue on the styled-components repo: styled-components/styled-components#1803 (comment)

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: microsoft/TypeScript#1213

@Naararouter
Copy link

Naararouter commented Jan 12, 2020

I've found the next solution for me. It seems, it fulfils a need task: styled-components/styled-components#1803 (comment)

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} />
        </>
    );
};

@melounek
Copy link
Author

melounek commented Jan 13, 2020

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} />;

Warning: as @tokland found later, this solution is throwing a runtime error and the solution from @choznerol 👇 below looks much better (using <StyledFoo<FC<Props<Bar>>> ... />)

@Naararouter
Copy link

@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.

@melounek
Copy link
Author

melounek commented Jan 13, 2020

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

@Naararouter
Copy link

@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

@melounek
Copy link
Author

@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.

@choznerol
Copy link

choznerol commented Oct 31, 2020

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

@tokland
Copy link

tokland commented Nov 20, 2020

@melounek, thanks, I tried your solution, it types fine. However, now I get the typical runtime warning that the component has been created dynamically. Am I missing something? (styled-components: 5.2.1).

@melounek
Copy link
Author

I just tested in freshly created app ((styled-components: 5.2.1 / "react-scripts": "4.0.0-next.98 / "typescript": "^4.1.0") and I didn't find any issue with starting or building the app

@tokland
Copy link

tokland commented Nov 20, 2020

melounek added a commit to melounek/test-styled-components that referenced this issue Nov 21, 2020
@melounek
Copy link
Author

You are probably right @tokland and there is something wrong with my solution. Based on what the official documentation says: WARNING: THIS IS VERY VERY BAD AND SLOW, DO NOT DO THIS!!!

But @choznerol 's solution probably solves our struggles. See my PR for your code if you see fit: https://github.com/tokland/test-styled-components/pull/1/files

@tokland
Copy link

tokland commented Nov 21, 2020

@melounek, I see, so we need to pass the generic type as React.FC<ComponentProps<T>>, that will work for me, thx!

@aaronmw
Copy link

aaronmw commented Mar 1, 2021

@melounek what do you recommend in today's world where React.FC has been deprecated?

@melounek
Copy link
Author

melounek commented Mar 1, 2021

@aaronmw You sure? I don't see any deprecated note about it. Maybe you mixed up with React.SFC? See here:

type FC<P = {}> = FunctionComponent<P>;

@aaronmw
Copy link

aaronmw commented Mar 1, 2021

@melounek Sorry, I was thinking of its removal from create-react-app. I was reading this whole thread just yesterday after trying to figure out the right way to do things, and with so many examples of React.FC in nearly every Typescript demo or answer, I am... Confused.

Here's the story of its removal from create-react-app: facebook/create-react-app#8177

@badsyntax
Copy link
Contributor

@melounek none of the workaround seems like a good solution. Can you re-open this issue and tag the styled-component TS authors so they're aware of this issue?

@melounek
Copy link
Author

You think the the @choznerol 's solution is not good enough? - using <StyledFoo<FC<Props<Bar>>> ... />

Could you elaborate what are you missing or how exactly should be fixed the styled-components before reopening it?

@badsyntax
Copy link
Contributor

badsyntax commented Apr 12, 2021

Perhaps i'm having a bad day and missing something obvious, but I don't see how that solution will work in this context: https://codesandbox.io/s/awesome-worker-c78m8?file=/src/App.tsx

FlatList expects a (generic) type to be able to type the data correctly.

@melounek
Copy link
Author

@badsyntax
Copy link
Contributor

@melounek guess i am having a bad day. many thanks, i appreciate it!

@ahoisl
Copy link
Contributor

ahoisl commented Jun 9, 2021

The solution posted by @choznerol (-> #39136 (comment)) seems to be broken with release 5.1.10 of @types/styled-components. Previous correctly typed generic styled components do not compile any more and I could not get it to work.

It's basically reverted to the former behavior as when not explicitly specifying the generic type 😒

Does anyone else experience the same problem with @types/styled-components 5.1.10?

The sandbox @badsyntax posted earlier also has errors when upgrading to 5.1.10, see here: https://codesandbox.io/s/peaceful-stallman-seji1?file=/src/App.tsx

@MauriceNino
Copy link
Contributor

What worked for me was the answer from this SO post: https://stackoverflow.com/a/67546231/9150652

interface IProduct {
  id: string;
  name: string;
}

const StyledFlatList = styled(FlatList as new () => FlatList<IProduct>)`
  background-color: #f7f7f7;
`

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

No branches or pull requests

10 participants