How to create stateless functional components that accept generics? - reactjs

Typescript generics can used to extend interfaces.
interface Sample1<P> {
prop1: P;
}
interface Sample2<P> extends Sample1<P> {
prop2: string;
}
But when I try to create a functional component that uses the generic interface, typescript throws error.
const SampleSFC: React.SFC<Sample2<P>> = () => <div />;
error TS2304: Cannot find name 'P'.
If I replace P with a known type like string the error goes away.
const SampleSFC: React.SFC<Sample2<string>> = () => <div />;
Instead of hard coding type of P, that totally kills the purpose of generics, I want to enable users of SampleSFC to set type of P.
How can I do that? If it's not possible then what alternate design I should follow that would let me have SFC with generic props.

Generic type variables can be used in function and type declarations like your interfaces Sample1 and Sample2.
But when you actually use/call/invoke generic types, you have to specify a concrete type argument instead of P.
React.SFC - the stateless function component type - is an interface declaration, so using it in an const assignment requires a concrete type for P.
The only solution that comes to my mind is to make the SFC look like a function type for the ts-compiler, since function declarations/expressions can
be assigned generic type parameters. See Typescript docs for some examples.
Generic SFC declaration
Leave out the React.SFC type annotation to your assignment const SampleSFC:
const SampleSFC = <P extends {}>(props: Sample2<P>) => <div>{props.prop1} </div>;
Note: The <P extends {}> part is needed due to JSX/TSX compatibility
issues with generics, see the end of this post.
This way, your component remains a plain function. If you need the additional members from React.SFC, add them to your props. E.g. if children needed:
const SampleSFC = <P extends {}>(props: Sample2<P> & { children?: ReactNode })
=> <div >{props.prop1} </div>;
Generic SFC usage
Since Typescript 2.9, the type parameter can be directly set in JSX.
const MyApp: React.SFC<{}> = (props) => {
return <SampleSFC<number> prop2="myString" prop1={1} />
}
Alternatives
1.) A switch to class components would be easy, if you don't like the quirks.
class SampleSFCClass<P> extends React.Component<Sample2<P>> {
render() {
return <div>{this.props.prop1}</div>
}
}
const MyApp2: React.SFC<{}> = (props) => {
return <SampleSFCClass<number> prop2="myString" prop1={1} />
}
2.) You could even wrap the SFC in another function.
const withGeneric: <P>() => React.SFC<Sample2<P>> = <P extends {}>() => {
return (props) => <div> {props.prop1} {props.prop2} </div>;
}
const SampleSFCCalled: React.SFC<Sample2<string>> = withGeneric<string>();
const MyApp: React.SFC<{}> = (props) => {
return <SampleSFCCalled prop2="myString" prop1="aString" />
}
It will work, but disadvantage might be slight performance decrease, because the SFC funtion is always recreated in each render cycle of the parent comp.
JSX/TSX compatibility issues with Generics:
In some constellations Typescript seems to have problems parsing generics in combination with JSX/TSX (up to most recent 3.0.1) due to ambiguous syntax. Compile error will then be:
"JSX element has no corresponding closing tag."
One of the contributors recommended to use the function syntax in this case (see issue).
When you stick to arrow function, workaround is to let the type parameter extend from object (shown here or here) to clarify its meant to be a generic lambda and not a JSX tag:
<P extends {}> or <P extends object>
Hope, that helps.

Generics only work within generic types. So when you are declaring a type interface Sample1<P>, then you can use P as a type within that declaration. However, the concrete type Sample1<P> does not actually exist. Instead P is a placeholder for a type when the Sample1 type is being used.
So using Sample1<P> outside of a generic type definition that also has a generic type argument P does not really work. You can only use actual non-generic types P then.
Thatโ€™s why Sample1<string> works, because string is a valid value for the type argument P in the Sample1<P> definition.
If you want so have a SampleSFC that works for any type P, then you could create it as a React.SFC<Sample1<any>>:
const SampleSFC: React.SFC<Sample1<any>> = () => <div />;

interface MyComponentProps<T> {
value?: T;
}
export const MyComponent = <T,>(
props: React.PropsWithChildren<MyComponentProps<T>>,
) => {
return (
<View>
{props.children}
<Text>{(props.value as unknown) as string}</Text>
</View>
);
};
then in the render method..
return (
<MyComponent<string> />
)

Related

React generic type prop must extend other type?

I'm kinda new to typescript so this one is confusing for me. I wanted to pass generic type into my hoc. I need to pass component props as a generic type to get Component with that types on it.
const withHOC = <T extends {}>(Component: ComponentType<T>) => (props: T) => {
return (
<Component {...props} />
)
}
//example: const MyView = withHoc<ViewProps>(View)
What I don't understand is that I can't just set <T> as generic type, I must set it as <T extends something>. With that said if I don't pass generic type (as shown in example) I wont get typescript warning for not passing generic type.Can someone explain me why is that happening?
I want it to look like this:
const withHOC = <T>(Component: ComponentType<T>) => (props: T) => {
return (
<Component {...props} />
)
}
So when I don't pass generic type when calling withHOC, it warns me there has to be one. I might me doing everything wrong and asking for something that is not achievable so correct me if I'm wrong.
There are two separate questions here:
1. <T> vs. <T extends something>
I can't just set <T> as generic type, I must set it as <T extends something>.
This is a syntactical issue when using arrow functions in .tsx files, as the <T> can be confused for a JSX expression. It is possible to use just <T> instead of <T extends something> if you convert from an arrow function to a traditional function declaration.
function withHOC<T>(Component: ComponentType<T>) {
return function (props: T) {
return (
<Component {...props} />
)
}
}
2. Requiring <T>
If I don't pass generic type (as shown in example) I wont get typescript warning for not passing generic type. Can someone explain me why is that happening?
I want it to look like this ... So when I don't pass generic type when calling withHOC, it warns me there has to be one. I might me doing everything wrong and asking for something that is not achievable so correct me if I'm wrong.
TypeScript will infer the type for T each time that you call withHOC based on the type of the Component argument that you call it with.
Let's say that you have the following code:
const Button = (props: ButtonProps) => (
<button {...props}/>
);
const WrappedButton = withHOC(Button);
The Button argument matches the type ComponentType<ButtonProps>, so TypeScript determines that the T for this withHOC call is ButtonProps.
This is called type inference and it is a powerful feature of TypeScript.
You don't need to explicitly set <T> using withHOC<ButtonProps>(Button) because it is already known.
There probably exists some tricky way to require an explicit <T> but it's not trivial and I don't see why you would want this.

Properly typing a React Typescript FunctionComponent whose parameterized type also requires a type parameter

There's probably an obvious answer to this, but I haven't been able to identify the right keywords to find it.
Suppose I have a type-parameterized FooProps, e.g.:
type FooProps<T> = { ... bar: T, ...}
I'm building a FunctionComponent that should take a FooProps as its type parameter.
The following is not correct:
const component: FunctionComponent<PropsWithChildren<FooProps<T>>> = <T, >(props: FooProps<T>) => {...}
because cannot find name 'T'.
However,
const component: FunctionComponent<PropsWithChildren<FooProps>> = <T, >(props: FooProps<T>) => {...}
is not correct, because Generic type 'FooProps' requires 1 type argument(s).
I can get away with just not annotating it, but the inferred type is then <T>(props: FooProps<T>) => JSX.Element, which wouldn't be so bad, except it means that I have to include the children member manually on FooProps<T>.
What is the correct way to annotate the type of this FunctionComponent when the type that satisfies its type parameter has its own parameterization?
Follow-on question:
Okay, so the first issue is that component's props parameter is still declared as FooProps<T> rather than PropsWithChildren<FooProps<T>>. Correcting that gets me most of the way there. However, I'd still like to be a bit more explicit if I can.
So I can do:
const component: FunctionComponent<PropsWithChildren<FooProps<any>>> =
<T, >(props: PropsWithChildren<FooProps<T>>) => {
but can't do:
const component: FunctionComponent<PropsWithChildren<FooProps<T>>> =
<T, >(props: PropsWithChildren<FooProps<T>>) => {
Is there a way to do this explicitly so that the generic type can feed through all the way? In my particular use case, I don't actually use T directly in this component, so marking it any is fine, but it'd be nice to know I can be explicit if I need to.
It is perfectly fine to wrap your FooProps into PropsWithChildren:
import React, { PropsWithChildren } from 'react'
type FooProps<T> = { bar: T, }
const Component = <T,>(props: PropsWithChildren<FooProps<T>>) => {
return null
}
const foo = <Component bar={1}>123</Component>
Playground
Your example does not work because you are trying to mix argument inference with explicit type declaration up front.
If you want to use generics, you are not allowed to use explicit type anymore. Using explicit type disabling type inference. Further more, using explicit type) means that you know all your types up front.

Inferences from JSX vs calling React Components as functions

I'm having a little trouble understanding how Typescript infers props in JSX. The issues has come up in the following context -- I'm trying to make a component that accepts another component as a prop, e.g. (a simplified example):
function WrapperComponent<T extends FooComponentType>(
{FooComponent}:{FooComponent:T}
){
return FooComponent({className:"my-added-class"})
}
Where FooComponentType is the type of a component that has a className prop:
type FooComponentType = (props:{className:string}) => JSX.Element
What I'd like, is for Typescript to disallow any attempt to pass in a component that doesn't have a className prop. Now, I recognize the typing above will not achieve that because React components are functions and so the props argument is covariant, i.e., a type that would be satisfied by a component with a className prop will also be satisfied by a component without one. Therefore, both of these components legitimately extend FooComponentType
const ComponentNoClass = ()=><div/>
const ComponentWClass = (props:{className:string})=><div/>
A workaround that I'm familiar with is to use a conditional time to essentially force the type to be bivariant by having some other type resolve to never if the passed component doesn't exactly match (i.e., if both types don't extend each other). My thought here was I could use such a type to make the return type of WrappedComponent be never if the passed component didn't have the className prop. So, to that end, I wrote the following helper type:
type JsxIfHasClassNameProps<
T extends FooComponentType
> = FooComponentType extends T ? JSX.Element : never
And used it to create a "smarter" version of WrappedComponent:
function CheckedWrapperComponent<T extends FooComponentType>(
{FooComponent}:{FooComponent:T}
):JsxIfHasClassNameProps<T>
{
return FooComponent({className:"my-added-class"}) as JsxIfHasClassNameProps<T>
}
I would think that would work, but it doesn't. Sort of... It doesn't work if I call CheckedWrapperComponent using JSX, but it does work if I call it as a normal function with arguments, namely:
// this type is JSX.Element as expected
const cjsx1 = <CheckedWrapperComponent FooComponent={ComponentWClass} />
// this type is also JSX.Element, but I want it to be never
const cjsx2 = <CheckedWrapperComponent FooComponent={ComponentNoClass} />
// this type is never as expected and desired
const cjsx3 = CheckedWrapperComponent({FooComponent:ComponentNoClass})
Can anyone explain what is going on here and how I can get the behavior I want?
Here is a sandbox link.
(BTW, I don't think this changes anything, but I had to call FooComponent as a function inside WrapperComponent to get around an error TS was giving me regarding the Intrinsic attributes it applies to JSX. I'm open to a better way to do that, but it's not a huge priority as that implementation is all hidden from the consumer. By contrast I can't reasonable expect consumers not to call CheckedWrapperComponent using JSX)
Edit
Just to clarify, my question is not about how to solve the contravariance issue generally. It's more specifically about how to solve it a way that is compatible with JSX. As noted in the second and third examples above:
// this type is also JSX.Element, but I want it to be never
const cjsx2 = <CheckedWrapperComponent FooComponent={ComponentNoClass} />
// this type is never as expected and desired
const cjsx3 = CheckedWrapperComponent({FooComponent:ComponentNoClass})
I can solve the issue if I pass in the component as a "normal" function argument. My question is why that stops working when I pass it in to JSX as a prop--and can this be achieved?
(Also, as noted in the comments, calling a functional component directly as is done in my examples is not the recommended way to do a non-JSX call, but I don't think that's relevant to my question which is really about TS. The same behavior occurs if you, properly, use React.createElement)
Check this out:
type FooComponentType = (props: { className: string }) => JSX.Element;
function wrapComponent<C extends FooComponentType extends C ? any : never>(
Wrapped: C
) {
return React.createElement(Wrapped, { className: "my-added-class" });
}
function ComponentNoClass(props: {}) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x + 1)}>
my classname is... oops I don't have a classname. [{state}]
</div>
);
}
function ComponentWClass(props: { className: string }) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x + 1)}>
my classname is {props.className}. [{state}]
</div>
);
}
function ComponentWClassPlus(props: { className: string; foo: number }) {
const [state, setState] = React.useState(0);
return (
<div onClick={() => setState((x) => x + 1)}>
my classname is {props.className}. [{state}]
</div>
);
}
export default function App() {
const [state, setState] = React.useState(0);
const jsx1 = wrapComponent(ComponentNoClass); // not fine
const jsx2 = wrapComponent(ComponentWClass); // fine
const jsx3 = wrapComponent(ComponentWClassPlus); // fine
return (
<div>
{jsx1}
{jsx2}
{jsx3}
<button onClick={() => setState((x) => x + 1)}>
increment outer count. [{state}]
</button>
</div>
);
}
What's going on here?
wrapComponent() is basically a "higher order component" function, that returns some rendered JSX from some component function/class that you call it with.
return React.createElement(Wrapped, { className: "my-added-class" });
The special sauce that gives you the type checking you crave is this: C extends FooComponentType extends C ? any : never.
It essentially says, "generic type C extends the conditional type (does FooComponentType extend the generic type C? If so, this type is any, otherwise it is never)".
It's a sort of counterintuitive way to get around the covariant issue you mentioned earlier, using a conditional type + never to assert the type. It feels circular somehow (C extends FooComponentType extends C???) but I guess because of the way TypeScript evaluates the conditional type from the inside out, it's fine?
You were pretty close with your implementation of CheckedWrapperComponent, but this way you cut out that middle man and only need to use the higher order component function.

Interface infer type based on a property it defines

When an interface exposes a generic that simply get's used to type one the properties. Is there a way to "use" that based on inference at a usecase?
Take a look at this:
please assume here that the Generic cannot be simply applied, and that the onClick will exist.
As you can see, the is property of my TestObject is a generic, that is statically given. Is there a way to put bounds around this so when the onClick wants an argument, it knows that the is property is div to therefore only allow value == 'div'.
My use case for this is in the React world where I want my component to be given a prop that defines its render (createElement), but need that to be typesafe for all handlers and attributes it applies. I suppose a generic would work, but that falls apart when sent into forwardRef.
Here is an example of what I have currently, and also where my predicament lies.
import { AllHTMLAttributes, createElement, forwardRef } from 'react';
interface Props<Element extends keyof JSX.IntrinsicElements>
extends Omit<AllHTMLAttributes<Element>, 'width' | 'height'> {
is?: Element;
className?: string;
}
// There is a little more going on inside the Component, but you get the gist.
const Box = forwardRef<HTMLElement, Props<'div'>>(({ is, children }, ref) =>
createElement(is, {
ref,
}, children));
As you can see from there, now the is prop is locked to just being a div.
TS currently doesn't support arbitrary generic value types except generic functions. Also, in a variable assignment like const x, the compiler cannot infer the type argument for T automatically.
In other words, you have to give TestObject a concrete type argument: const x: TestObject<"div">. Your case still compiles, as the given default "div"|"a" for T is used, when nothing is specified. Alternatively you could use a factory function to initializex, but here I would just go with the former for simplicity.
The issue with React.forwardRef is related to above topic, albeit a bit more complex.
React.forwardRef cannot output a generic component with current React type definitions - I have mentioned some workarounds in the linked answer. The simplest workaround for you is to use a type assertion:
const Box = forwardRef<HTMLElement, Props<keyof JSX.IntrinsicElements>>(({ is, children }, ref) =>
is === undefined ? null : createElement(is, { ref, }, children)) as
<T extends keyof JSX.IntrinsicElements>(p: Props<T> &
{ ref?: Ref<HTMLElementFrom<T>> }) => ReactElement | null
// this is just a helper to get the corresponding HTMLElement, e.g. "a" -> HTMLAnchorElement
type HTMLElementFrom<K extends keyof JSX.IntrinsicElements> =
NonNullable<Extract<JSX.IntrinsicElements[K]["ref"], React.RefObject<any>>["current"]>
type AnchorEle = HTMLElementFrom<"a"> // HTMLAnchorElement
This will make your Box generic and you could create both div and a Boxes:
const aRef = React.createRef<HTMLAnchorElement>()
const jsx1 = <Box is="a" ref={aRef} onClick={e =>{}} />
// is?: "a" | undefined, ref: RefObject<HTMLAnchorElement>, onClick?: "a" callback
const divRef = React.createRef<HTMLDivElement>()
const jsx2 = <Box is="div" ref={divRef} onClick={e =>{}} />
// is?: "div" | undefined, ref: React.RefObject<HTMLDivElement>, onClick?: "div" callback
Sample

Typescript with React - use HOC on a generic component class

I have a generic React component, say like this one:
class Foo<T> extends React.Component<FooProps<T>, FooState> {
constructor(props: FooProps<T>) {
super(props);
render() {
return <p> The result is {SomeGenericFunction<T>()}</p>;
}
}
I also have a HOC that looks similar to this one (but is less pointless):
export const withTd =
<T extends WithTdProps>(TableElement: React.ComponentType<T>): React.SFC<T> =>
(props: T) => <td><TableElement {...props}/></td>;
But when I use a component like this:
const FooWithTd = withTd(Foo);
There is no way to pass the type argument, as you can do neither withTd(Foo<T>), nor can you do FooWithTd, the type is always wrong.
What is the proper way to do that?
EDIT: The problem is that I want to be able to have something like <FooWithTd<number> {...someprops}/> later on, as I don't know the desired type for T in the HOC.
You can wrap your component which is created from a HOC into another component. It would look something like this:
class FooWithTd<T> extends React.Component<SomeType<T>> {
private Container: React.Component<SomeType<T> & HOCResultType>;
constructor(props:SomeType<T>){
super(props);
this.Container = withTd(Foo<T>);
}
render() {
return <this.Container {...this.props} />;
}
}
Remember, you probably don't want the HOC inside your render function because it means that the component will be recreated every each render.
Thanks for asking this question. I just figured out a way to specify a type parameter to a component after wrapping it with an HOC and I thought I'd share.
import React from 'react';
import withStyles from '#material-ui/core/styles/withStyles';
import { RemoveProps } from '../helpers/typings';
const styles = {
// blah
};
interface Props<T> {
classes: any;
items: T[];
getDisplayName: (t: T) => string;
getKey: (t: T) => string;
renderItem: (t: T) => React.ReactNode;
}
class GenericComponent<T> extends React.Component<Props<T>, State> {
render() {
const { classes, items, getKey, getDisplayName, renderItem } = this.props;
return (
<div className={classes.root}>
{items.map(item => (
<div className={classes.item} key={getKey(item)}>
<div>{getDisplayName(item)}</div>
<div>{renderItem(item)}</div>
</div>
))}
</div>
);
}
}
// ๐Ÿ‘‡ create a `type` helper to that output the external props _after_ wrapping it
type ExternalProps<T> = RemoveProps<Props<T>, 'classes'>;
export default withStyles(
styles
)(GenericComponent) as <T extends any>(props: ExternalProps<T>) => any;
// ๐Ÿ‘† cast the wrapped component as a function that takes
// in a type parameter so we can use that type
// parameter in `ExternalProps<T>`
The main idea is to cast the wrapped component as a function that takes in a type parameter (e.g. T) and use that type parameter to derive the external props after the component has been wrapped.
If you do this, then you can specify a type parameter when using the wrapped version of GenericComponent e.g.:
<GenericComponent<string> {/*...*/} />
Hopefully the code is explanatory enough for those who still have this problem. In general though, I consider this relatively advanced typescript usage and it's probably easier to use any instead of a generic parameter in the props
Workaround: simple case
If your component's type parameter is used only for passing it to props, and users of the component do not expect it having any functionality beyond just passing props and rendering, you can explicitly hard-cast the result of your hoc(...args)(Component) to React's functional component type, like this:
import React, {ReactElement} from 'react';
class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }
const kindaFixed = myHoc(...args)(MyComponent) as unknown as <T>(props: MyProps<T>) => ReactElement;
Workaround: more complex and with some runtime costs
You can use fabric-like function, supposed here:
class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }
export default function MyComponentFabric<T>() {
return hoc(...args)(MyComponent as new(props: MyProps<T>) => MyComponent<T>);
}
This one will require you to create new version of wrapped component for each type you use it with:
import MyComponentFabric from '...whenever';
const MyComponentSpecificToStrings = MyComponentFabric<string>();
It will allow you to access all public instance fields and methods of your component.
Summary
I faced this issue when tried to use connect from react-redux on my ExampleGenericComponent<T>. Unfortunatelly, it cannot be fixed properly until TypeScript will support HKT, and any HOC you use will update its typings respecting this feature.
There is possibly no correct solution (at least for now) for usages beyond just rendering, when you need to access component instance fields and methods. By 'correct' I mean 'without ugly explicit typecasts', and 'with no runtime cost'.
One thing you can try is to split your class-component into two components, one that will be used with HOC, and other that will provide fields and methods that you need.
Just stumbled upon this as well and thought I'd share what I came up with in the end.
Building on what #rico-kahler provided, my approach mapped to your code would be
export const FooWithTd = withTd(Foo) as <T>(props: FooProps<T>) => React.ReactElement<FooProps<T>>;
which you can then use like this
export class Bar extends React.Component<{}> {
public render() {
return (
<FooWithTd<number> />
);
}
}
In my case, I have defaultProps as well and I inject props by ways of another HOC, the more complete solution would look like this:
type DefaultProps = "a" | "b";
type InjectedProps = "classes" | "theme";
type WithTdProps<T> = Omit<FooProps<T>, DefaultProps | InjectedProps> & Partial<FooProps<T> & { children: React.ReactNode }>;
export const FooWithTd = withTd(Foo) as <T>(props: WithTdProps<T>) => React.ReactElement<WithTdProps<T>>;
EDIT:
After some changes to your code, it was only a wrong constraint T in your withTd function.
// I needed to change the constraint on T, but you may adapt with your own needs
export const withTd = <T extends FooProps<WithTdProps>>(
TableElement: React.ComponentType<T>
): React.SFC<T> => (props: T) => (
<td>
<TableElement {...props} />
</td>
)
// Explicitly typed constructor
// Removed after EDIT
//const FooW = Foo as new (props: FooProps<WithTdProps>) => Foo<WithTdProps>
// Inferred as React.StatelessComponent<FooProps<WithTdProps>>
const FooWithTd = withTd(Foo)
No longer relevant after EDIT :
You may find more information at this issue https://github.com/Microsoft/TypeScript/issues/3960

Resources