How to correctly type a generic React function component in TypeScript - reactjs

In the following TypeScript Playground example I tried to generalise the function component Element into a GenericElement component but TypeScript complains about the syntax.
How to correctly type a generic react function component in TypeScript using the React.FC type definition approach?
import React from 'react';
type PropsType = {
id: string,
value: string,
};
type GenericPropsType<keyType> = {
id: keyType,
value: string,
};
const Element: React.FC<PropsType> = ({ id, value }) => {
return <div>{id.toString()}={value}</div>;
};
const GenericElement: React.FC<GenericPropsType<keyType>> = <keyType = string>({ id, value }) => {
return <div>{id.toString()}={value}</div>;
};
Type 'Element' is not assignable to type 'FC<GenericPropsType<any>>'.
Type 'Element' provides no match for the signature '(props: PropsWithChildren<GenericPropsType<any>>, context?: any): ReactElement<any, any> | null'.
Cannot find name 'keyType'.
Property 'keyType' does not exist on type 'JSX.IntrinsicElements'.
Cannot find name 'id'.
Left side of comma operator is unused and has no side effects.
Cannot find name 'value'.
Cannot find name 'id'.
Cannot find name 'value'.
Identifier expected.
Unexpected token. Did you mean `{'>'}` or `>`?
Expression expected.
Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
JSX element 'keyType' has no corresponding closing tag.
'</' expected.
'Element' is declared but its value is never read.

I think it works using Higher-order function:
import React, { FC } from 'react';
type GenericPropsType<T = any> = {
id: T,
value: string,
};
const HoC = <T,>(): FC<GenericPropsType<T>> => (props) => <div></div>
const WithString = HoC<string>() // React.FC<GenericPropsType<string>>
Drawback: you have function overhead only because of type
I don't believe my answer is helpful, because you should explicitly define generic type, or you need to pass an argument in order to infer it:
const HoC = <T,>(a:T): FC<GenericPropsType<T>> => (props) => <div></div>
const WithString = HoC('a') // React.FC<GenericPropsType<string>>
Otherwise, I suggest you to use #Aleksey L.'s solution.

Based on the explanation of #Aleksey L., I came to the following complete example that might be helpful to others:
import React from 'react';
import ReactDOM from 'react-dom';
type GenericPropsType<keyType> = {
id: keyType,
value: string,
};
const GenericElement = <keyType extends string | number = string>({
id,
value = ''
}: GenericPropsType<keyType>): JSX.Element => {
return (<div>{id}={value}</div>);
};
const UseGenericElement: React.FC = () => {
return (<div><GenericElement id={4711} value="4711" /></div>);
};
ReactDOM.render(
<div><UseGenericElement /></div>,
document.getElementById('root');
);

Related

Problem with typescript error when create react generic component

Recently, I encountered some issues while using React. when I called my react generic component, BaseBlock.
I had already provided a type, but TypeScript threw an error saying
Expected 0 type arguments, but got 1.ts(2558)
I would like to ask how to solve this situation.
const items = ['test1', 'test2', 'test3'];
const renderComp = (item: any) => {
return <div>{item}</div>
}
<BaseBlock<string> items={items}>{renderRow}</BaseBlock>
^
error
This is my BaseBlock component
import React from 'react';
interface Props<T> {
id?: string;
items?: T[];
children?: (item: T, index: number) => React.ReactNode;
parentId?: string;
contentId?: string[];
}
const BaseBlock = React.forwardRef(function BaseBlock<T>(
{ items, children }: React.PropsWithChildren<Props<T>>,
ref: React.ForwardedRef<HTMLDivElement>
): React.ReactElement | null {
return (
<div data-testid="base-block" ref={ref}>
{items && children && items.map(children)}
</div>
);
});
export type BaseBlockProps<T> = React.PropsWithRef<Props<T>>;
export default BaseBlock;
I give my component string type. Excepted no error and render this component. It just render, but typescript give me error
Expected 0 type arguments, but got 1.ts(2558)
I think it is that I give the <string> type for my comp, but this comp should not use type, it excepted 0 argument on this comp, so how can I solved?
I solved this problem by #Linda Paiste comment
and reference this question.
Finally, I used global type augmentation and solved it.
declare module 'react' {
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}

How to define types for generic react Component?

I struggle to figure out how to type things properly.
I have a generic IPost type declared as:
export enum PostKind {
Message = "message",
Food = "food",
}
export type IPost<T extends PostKind> = {
type: T;
content: PostContent<T>; // not including the code for this type since is not relevant for now.
};
I have specific post types that extend this type:
export type IMessagePost = IPost<PostKind.Message> & {
messageLength: number;
};
export type IFoodPost = IPost<PostKind.Message> & {
image: string;
};
I have components for each specific post type:
export const MessagePost = (props: IMessagePost) => {
return <div>{props.messageLength}</div>;
};
export const FoodPost = (props: IFoodPost) => {
return <div>{props.image}</div>;
};
All good so far. New I want a generic Post component that takes an IPost param and displays the correct component that matches the PostKind.
const componentMap: Record<
PostKind,
React.FC<IMessagePost> | React.FC<IFoodPost>
> = {
[PostKind.Message]: MessagePost,
[PostKind.Food]: FoodPost,
};
export const Post = (props: IPost<PostKind>) => {
const Component = componentMap[props.type];
return <Component {...props} />; // typescript error here: Type 'PostKind' is not assignable to type 'PostKind.Message'
};
Something is worng with my types, and I cannot find a proper solution. Typescript shows the following error:
Type '{ type: PostKind; }' is not assignable to type 'IPost<PostKind.Message>'.
Types of property 'type' are incompatible.
Type 'PostKind' is not assignable to type 'PostKind.Message'.
You can check the full code, and see the error here:
Please don't suggest solution that uses types like unknown any ElementType ReactNode, or using the as keyword. I want everything to be typesafe.
So, I think in order for it to be sensible, the definition should be like this, right?
export type IFoodPost = IPost<PostKind.Food> & {
image: string;
};
Then, you can replace the last part with this:
const componentMap = {
[PostKind.Message]: MessagePost,
[PostKind.Food]: FoodPost,
};
export const Post1 = <A extends IPost<B>, B extends PostKind & keyof C, C extends Record<B, (a: A) => JSX.Element>>(props: A, rec: C) => {
const Component = rec[props.type]
return React.createElement(Component, props)
};
export const Post = <A extends IPost<PostKind>>(props: A) => {
return Post1(props, componentMap)
};
Given that, the <Post /> component will work if you type it explicitely:
const c1 = <Post<IMessagePost> type={PostKind.Message} messageLength={88} />
const c2 = <Post<IFoodPost> type={PostKind.Food} image={"xxxx"} />
const c3 = <Post<IFoodPost> type={PostKind.Food} /> // fails to typecheck
const c4 = <Post<IFoodPost> type={PostKind.Food} messageLength={5} /> // fails to typecheck

React.createElement with typescript error when trying to pass props

I am trying to create dynamic components in react + typescript but I am struggling to correctly type the props on it.
Here is a simpler version of what I'm doing.
interface ComponentProps {
title: string;
}
interface ArrInterface {
component: FC<ComponentProps>;
title: string
}
const Arr: ArrInterface = [
{ component: FooComponent, title: "foo"},
{ component: FooComponent, title: "bar"}
];
const FooComponent: FC<ComponentProps> = ({title}) => {
return <div>{title}</div>
}
const BarComponent: FC = () => {
return sectionsArr.map((section) => {
const {component, title} = section;
return React.createElement(component as FC, { title })
})
}
this is the error I am getting :
Argument of type '{ title: string; }' is not assignable to parameter of type ' Attributes'.
Object literal may only specify known properties, and 'title' does not exist in type 'Attributes'
it works if I do a ts-ignore on top of that line btw :)
Can someone please help me out on this one? What am I missing here?
I found the solution myself :)
For those ending up with the same issue as I did, it turns out you need to pass the props type to createElement function.
So basically instead of doing
return React.createElement(component as FC, { title })
this should fix the issue
return React.createElement<ComponentProps>(component as FC, { title })
The signature that TS uses for your React.createElement is
function createElement<P extends {}>(
type: FunctionComponent<P>,
props?: Attributes & P | null,
...children: ReactNode[]): FunctionComponentElement<P>;
In your React.createElement(component as FC, { title }) call, you're passing a value (component) with the type FC as the argument named type. FC is defined as
type FC<P = {}> = FunctionComponent<P>;
So since you asserted component as FC — without a type argument — TS now uses the default type for the type argument P, namely {}. Now the type of the props argument is Attributes & {}, which is the same as just Attributes. And just like TS tells you, the property 'title' does not exist in type 'Attributes'.

Is there a way to extract the type of the props of a JSX Element?

My intent is to extract the props out of a given JSX element, is it possible?
This was pretty much my failed attempt...
Thanks in advance for any help ;)
function getComponentProps<T extends React.ReactElement>(element: T): ExtractProps<T>;
function Component({ name }: { name: string }) {
return <h1>{name}</h1>;
}
type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends React.ComponentType<infer TProps>
? TProps
: TComponentOrTProps;
const componentProps = getComponentProps(<Component name="jon" />); //type is JSX.Element
For the most part, you can't do this.
In theory, the React.ReactElement type is generic with a type parameter P that depends on the props. So if you were to have a strongly-typed element then you could work backwards.
type ElementProps<T extends React.ReactNode> = T extends React.ReactElement<infer P> ? P : never;
In reality, you will only get a correct props type if you create your element through React.createElement rather than JSX.
Any JSX element <Component name="John" /> just gets the type JSX.Element which obviously has no information about the props so you cannot work backwards from that to a props type.
const e1 = React.createElement(
Component,
{ name: 'John' }
)
type P1 = ElementProps<typeof e1> // type: {name: string}
console.log(getElementProps(e1)); // will log {name: "John"}
const e2 = <Component name="John" />
type P2 = ElementProps<typeof e2> // type: any
console.log(getElementProps(e2)); // will log {name: "John"}
Playground Link
It is much easier to approach the situation from a different angle. You will be able to derive the correct props type if your function takes a component like Component or div rather than a resolved element. You can use the ComponentProps utility type for function and class components and the JSX.IntrinsicElements map for built-in ones.
You can extract type of the Props for any component using
React.ComponentProps<typeof T>
You can refer this TS Playground for more options
import * as React from 'react';
type TProps = {
name:string;
age:number;
isStackoverflow:boolean;
}
const App = (props:TProps) => <div>Hello World</div>;
//You can extract any components props like this.
type AppProps = React.ComponentProps<typeof App>;
`
This is not possible for getComponentProps(<Component name="jon" />);, since written out JSX-Elements always result in the JSX.Element-type which doesn't give any additional type information which you could extract. It would be possible if you extract it from the component function itself:
export function Component({ name }: { name: string}) {
return <h1>{name}</h1>;
}
function getComponentProps<T extends (...args: any[]) => JSX.Element>(element: T): Parameters<T>[0] {
return null as any;
}
const test = getComponentProps(Component); // { name: string;}
This solution uses the utility type parameter, which infers all arguments from a function. We then index the first argument since the prop object is the first argument of a pure jsx function. Class components would need a different solution, though.

How to properly type define event.target.value?

I'm authoring a node package but I'm having bit of an issue with my typescript definitions. To be more specific I find the definition of event.target.value super confusing
Issue description:
I have the following event handler:
import { ChangeEvent, useState } from 'react'
type FieldEvent = ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
export const useField = <T>(input: T) => {
const [value, setValue] = useState<T>(input)
const handleChange = (event: FieldEvent) => {
const { name, value: eventValue } = event.target
// #ts-expect-error
setValue(eventValue)
}
return [input, handleChange]
}
The expression setValue(eventValue) results in the following error:
Argument of type 'string' is not assignable to parameter of type 'SetStateAction<T>'.
I was a bit surprised by this, given a lot of exported components use different event.target.value. Eg date-picker return Date type, select Object, etc.
Issue investigation
Naturally I went to check the imported ChangeEvent react exports to see if it has correct definitions, but this appears to be correct
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
target: EventTarget & T;
}
so according to this definition it should inherit the type of the Element that was passed to the SyntheticEvent
so I followed the chain to the HTMLInputElement declaration located in node_modules/typescript/lib/lib.dom.d.ts which is where the crux of the issue lies
interface HTMLInputElement extends HTMLElement {
value: string
//... rest
}
I checked back and it appears all the native <input> elements default to string as their value type, which I guess make sense.
Solving the issue
Obviously this is not ideal, given this does not represent the event.target.value behavior in a lot of the reactjs projects that use third-party-packages (which my package is supposed to support). Consider the following codesandbox
The returned event.target.value is as you'd expect of typeof number
that leads me to the question, should I simply override the ChangeEvent with the following definition?
ChangeEvent<{ value: T, name: string } & HTMLInputElement>
or would this be considered a bad practice? Or is there some better way to go about doing this altogether?
handleChange is not match to required params.
I've tried and it worked:
export default function App() {
const [selected, setSelected] = useState(1);
const handleChange = (e: ChangeEvent<{
name?: string | undefined,
value: unknown | number
}>, child: React.ReactNode) => {
setSelected(e.target.value as number);
};
return (
<Select value={selected} onChange={handleChange}>
<MenuItem value={1}>One</MenuItem>
<MenuItem value={2}>Two</MenuItem>
<MenuItem value={3}>Three</MenuItem>
</Select>
);
}
Alright, I'm not 100% sure if this is the correct approach but it seems to work fine for my use-case, albeit the typing seems a tiny bit odd, but basically I'm overwriting the passed type argument to ChangeEvent and extending it by a union of one the HTML elements.
export type FieldEvent<T> = ChangeEvent<
{ value: T, name?: string } &
(HTMLInputElement | HtmlTextAreaElement | HTMLSelectElement)
>
This overwrites the type definition of the ChangeEvent, then you just need to create a handler function that extends the type argument
export type FieldHanderFunction<T> = (event: FieldEvent<T>) => void
so then inside my hook, it basically comes down to:
const useField<T> = (input: T) => {
const handleChange = (event: FieldEvent<T>) => {
// ...
}
}

Resources