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) */}
</>
);
@types/styled-components package and had problems.Definitions by: in index.d.ts) so they can respond.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
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 while8is invalid
P.s. I also use a similar workaround for antd/select
ValueType=number
Environment:
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 thestyled()component by a generic one, which handles the generics-typing.Closing.