React.createElement with typescript error when trying to pass props - reactjs

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

Related

How to write a Typescript React Component Wrapper

A custom ReactJS wrapper component should be used to wrap code and only render it, if the user has the required permission. Therefore, it needs to accept two parameters, the children that shall be rendered as well as the permission it needs to check for.
The idea is to use it like this:
const AdminTools = () => {
return (
<RequirePermission require_permission="ui:see_admin_menu">
<>
<h1>Sudo mode</h1>
<Link href="/intern/users/">
<a>
<ActionCard link>User management</ActionCard>
</a>
</Link>
</>
</RequirePermission>
);
};
What I came up with so far is the following code:
const RequirePermission = (children: React.FC<{}>, required_permission: string) => {
const user = useContext(AuthContext);
let hasPermission = false;
if (user.state == AuthState.Authorized) {
user.user?.realm_access?.roles.forEach(role => {
if (permissions[role].includes(required_permission)) {
hasPermission = true;
}
});
}
if (hasPermission) {
return children;
} else {
return <div />;
}
};
export default RequirePermission;
When using the code snippet as described above, the following error is thrown:
Type '{ children: Element; require_permission: string; }' is not assignable to type 'IntrinsicAttributes & FC<{}>'.
Property 'children' does not exist on type 'IntrinsicAttributes & FC<{}>'.ts(2322)
'RequirePermission' cannot be used as a JSX component.
Its return type 'FC<{}> | Element' is not a valid JSX element.
Type 'FunctionComponent<{}>' is missing the following properties from type 'Element': type, props, keyts(2786)
I don't really understand the error message to be frank. Any help would be much appreciated.
//Edit:
Error messages given by proposed code:
This JSX tag's 'children' prop expects a single child of type 'ReactChildren', but multiple children were provided.ts(2746)
and
'RequirePermission' cannot be used as a JSX component.
Its return type 'Element | ReactChildren' is not a valid JSX element.
Type 'ReactChildren' is missing the following properties from type 'Element': type, props, key
Try this, importing ReactChildren from react.
I wish i could explain this better, but i know typescript is expecting a JSX/TSX element, so we could use ReactElement for the return type.
For a better explanation on the children type
https://stackoverflow.com/a/58123882/7174241
import React, { ReactChildren, ReactElement, ReactNode } from "react";
interface RequireType {
children: ReactChildren | ReactNode | ReactElement;
required_permission: string;
}
const RequirePermission = ({ children, required_permission }: RequireType):ReactElement => {
const user = useContext(AuthContext);
let hasPermission = false;
if (user.state == AuthState.Authorized) {
user.user?.realm_access?.roles.forEach((role) => {
if (permissions[role].includes(required_permission)) {
hasPermission = true;
}
});
}
return <>{hasPermission ? children : null}</>
};
export default RequirePermission;

How to correctly type a generic React function component in TypeScript

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');
);

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.

React typescript and children

I try to dynamically check in typescript the types of react children components.
The following code is working pretty well, however seems typescript does not want me to destructure children.
I get the Typescript error :
TS2339: Property 'type' does not exist on type 'ReactNode'.
What can i do t get rid of the typescript error instead of using // #ts-ignore.
import * as React from 'react';
export interface AuxProps {
children: React.ReactNode[]
}
export const Message: React.FC<AuxProps> = ({
children,
}: AuxProps) => {
const test = children.filter(({ type }) => type === Test);
return (
<div>
{test}
<div/>
);
};
You can't read type from ReactChild, because ReactChild is a union and not every member of that union has a property called type.
In fact, only ReactElements do.
The solution is to check for two things inside your predicate function:
Is this child a ReactElement?
If yes, then is that element of the desired type?
Code:
const test = children.filter(child => React.isValidElement(child) && child.type === 'Test');
This is because the default ReactNode does not have the field type.
You can simply add that key by using the & functionality:
export interface AuxProps {
children: (React.ReactNode & {type: string})[]
}
This will add the type to the elements.

React useState empty array type

interface Crumb {
title: string;
url: string;
}
interface Crumbies {
crumbsArray: Crumb[];
}
// component
const [breadcrumbs, setBreadcrumbs] = useState<Crumbies>([]);
I'm getting an error:
TS2345: Argument of type 'never[]' is not assignable to parameter of type 'Crumbies | (() => Crumbies)'.   Type 'never[]' is not assignable to type '() => Crumbies'.     Type 'never[]' provides no match for the signature '(): Crumbies'.
How to provide a correct typings for an empty array in useState hook?
UPDATE 1
const Breadcrumbs: React.FC<Crumbies> = ({ crumbsArray }) => {}
That's why i've created another interface Crumbies to wrap Crumb. Is there a better approach to this?
The interface called Crumbies requires you to pass an object with a field crumbsArray:
const [breadcrumbs, setBreadcrumbs] = useState<Crumbies>({crumbsArray: []});
If you want to simply have an array of Crumb you don't need to create a new interface, you can simply do it in useState:
const [breadcrumbs, setBreadcrumbs] = useState<Crumb[]>([]);
This will initialise it with an empty array.

Resources