Typing a dynamic tag in React with TypeScript? - reactjs

How do I type a dynamic tag in React with TypeScript? Given this code:
interface CompProps {
tag: string;
}
const MyComponent: React.FunctionComponent<CompProps> = ({
tag = "div",
children
}) => {
const Wrapper = tag;
return <Wrapper>{children}</Wrapper>;
};
I am getting this error:
Type '{ children: ReactNode; }' has no properties in common with type 'IntrinsicAttributes'. ts(2559)
It seems to me I have to add proper types but I cannot figure out which.

You can pass in a string as a tag name and use that as you have, but you need to type it properly to get type checking to work. tag should be a key of JSX.IntrinsicElements.
interface CompProps {
tag: keyof JSX.IntrinsicElements;
}
const MyComponent: React.FunctionComponent<CompProps & React.HTMLAttributes<HTMLOrSVGElement>> = ({
tag: Wrapper = "div",
children,
...rest
}) => {
return <Wrapper {...rest}>{children}</Wrapper>;
};
Playground Link

Using A Type definition For All HTML Elements
In order to allow all HTML elements to be used as your tag, you can utilize the keys of the IntrinsicElements interface defined in the JSX namespace. IntrinsicElements appears to contain a mapping of HTML element tags to their respective attributes (includes element-specific attributes). To utilize these keys we can do the following:
interface Props {
tag?: keyof JSX.IntrinsicElements
}
What if I want to allow React components to be used as the tag?
React defines two interfaces: ComponentClass and FunctionComponent. React also defines a union of these two interfaces that allows you to specify any React component: ComponentType. We can create a union of this and our last definition to allow both components and HTML tags.
import { ComponentType } from 'react';
interface Props {
tag?: ComponentType | keyof JSX.IntrinsicElements;
}
Well, now I have a tag, what about HTML attributes?
If you want to allow all other HTML attributes to be allowed you can either extend React.HTMLAttributes<Element> to get all of the shared HTML attributes (no element-specific ones) or you can introduce a generic and utilize JSX.IntrinsicElements.
The second option is more complex and comes with a few caveats. You have to use type instead of interface to extend/intersect your Props and the specific attributes defined on a key in JSX.IntrinsicElements. You will also need to use generics on your function so that you can pass them to your Props type which means you can no longer use React.FunctionComponent<Props> since that happens before access to any generics. This means you'll want to add children to your Props definition.
That was a lot of words which I believe are better explained with this example:
// Define our Props type to allow the specifying of a Tag for HTML attributes
// Also define children as React does with React.ReactNode
type Props<Tag extends keyof JSX.IntrinsicElements> = {
tag?: ComponentType | keyof JSX.IntrinsicElements;
children?: ReactNode;
} & JSX.IntrinsicElements[Tag];
// Define our generic (Tag) again here and give it our default value
// Don't forget to specify the type Props<Tag> at the end of your function's arguments
// Then we can spread all props to the tag/Wrapper
function MyComponent<Tag extends keyof JSX.IntrinsicElements = 'div'>({ tag: Wrapper = 'div', ...props }: Props<Tag>) {
return <Wrapper {...props} />;
}
// Example usage, noValidate is typed as
// (JSX attribute) React.FormHTMLAttributes<HTMLFormElement>.noValidate?: boolean | undefined
<MyComponent<'form'> tag="form" noValidate>
{/* My Form Stuff */}
</MyComponent>;
// You don't need to specify 'div' since it is the default
<MyComponent id="page">
<p>Just a paragraph inside of a regular div</p>
</MyComponent>;

I had a similar problem where I tried to generate a dynamic heading tag based on a passed 'level' prop. It also generated the "Property X does not exist on type IntrinsicAttributes" error.
The code that generated the error was the following;
// Heading.tsx
import React, { FunctionComponent, ReactNode } from 'react';
interface PropsType {
level: 1 | 2 | 3 | 5 | 6;
children?: ReactNode;
}
type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
const HeadingComponent: FunctionComponent = ({
level,
children = null
}: PropsType) => {
const Tag = `h${level}` as HeadingTag;
return (
<Tag>
{children}
</Tag>
);
};
export default HeadingComponent;
// And I used this component all over my codebase like this;
// HomePage.tsx
<Heading level={1}>
This Is A Title
</Heading>
I solved this by changing:
const HeadingComponent: FunctionComponent = ({
... // removed for brevity
}: PropsType) => {
... // removed for brevity
};
to:
const HeadingComponent: FunctionComponent<PropsType> = ({
... // removed for brevity
}) => {
... // removed for brevity
};

const YourComponent: React.FC<Props> = ({ tag: Tag = 'button', children, ...props }) => (
<Tag {...props}>
{children}
</Tag>
);
type Props = {
tag?: keyof JSX.IntrinsicElements;
} & React.HTMLAttributes<HTMLOrSVGElement>;
This works well for me.

Simple method to use dynamic tag name in React with TypeScript:
export default function Text(props: TextProps) {
const { text, header } = props;
let Tag: string;
if (!header) Tag = "span";
else Tag = `h${header}`;
const ConstTag = Tag as "span" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
return <ConstTag>{text}</ConstTag>;
}

Related

Fixing types for mapping over object to create React components

I really like uing this pattern for rendering similar components and using a _type prop to distinguish it and pass it down to the correct component.
However, I've found it difficult to add in the types correctly and was wondering if you guys could help. I have some questions;
Is the BlockMap type correct?
What type should I be using for ResolvedBlock?
Or generally, is there a better way of writing the types (without changing this structure?)
import React from 'react'
import { ImageBlock } from '/ImageBlock' // Assume all components are imported from whereever
type BlockType =
| 'imageBlock'
| 'formBlock'
| 'statisticBlock'
| 'videoBlock'
| 'quoteBlock'
interface Block {
_type: BlockType
_key: string
heading?: string
backgroundColor?: string
theme?: 'dark' | 'light'
}
type BlockMap = Record<BlockType, JSX.Element> // Is this type correct?
const blockMap:BlockMap = {
imageBlock: ImageBlock,
formBlock: FormBlock,
statisticBlock: StatisticBlock,
videoBlock: VideoBlock,
quoteBlock: QuoteBlock,
}
interface Props {
className?: string
blocks: Block[]
}
export function BlocksBuilder({
blocks = [],
className = ``,
}: Props):JSX.Element {
return (
<>
{blocks.map(block => {
const ResolvedBlock = blockMap[block._type] // What type should ResolvedBlock be?
if (!ResolvedBlock) return null
return (
<ResolvedBlock
className={className}
block={block}
key={block._key}
/>
)
})}
</>
)
}
It's a good pattern, and your type is close, but you will want to define the shape of your components instead.
type BlockMap = Record<BlockType, (props: any) => JSX.Element>
You could also define the props for Block components, and use that type for each of your components
interface BlockProps {
key: string;
block: Block;
classname: string;
}
type BlockComponent = (props: BlockProps) => JSX.Element;
export const ImageBlock: BlockComponent = (props) => {
return <></>;
};
type BlockMap = Record<BlockType, BlockComponent>

Material UI - Type 'string' is not assignable to type Icon

I'm coding a component that will return the icon corresponding to the passed prop as follows (simplified version of my app):
import * as icons from "#mui/icons-material";
interface IGenericIconProps {
iconName: keyof typeof icons;
}
export const GenericIcon = ({ iconName }: IGenericIconProps): JSX.Element => {
const Icon = icons[iconName];
return <Icon />;
};
import GenericIcon from './GenericIcon';
interface IUseGenericIconProps {
randomIcon: string; // the error goes away if I change this to 'any'
}
const UseGenericIcon = ({randomIcon}: IUseGenericIconProps): JSX.Element => {
return (
<GenericIcon iconName={randomIcon}/>
)
}
Then in another file I have
import UseGenericIcon from './UseGenericIcon';
enum MyIcons {
Task = "TaskOutlined",
Reminder = "AlarmOutlinedIcon",
Other = "AnnouncementOutlinedIcon",
}
const Calendar = () => {
return (
<UseGenericIcon randomIcon={MyIcons.Task}/>
)
}
This ends up throwing an Typescript error:
Type 'string' is not assignable to type '"Abc" | "AbcOutlined" | "AbcRounded" | "AbcSharp" | "AbcTwoTone" | "AcUnit" | "AcUnitOutlined" | "AcUnitRounded" | "AcUnitSharp" | "AcUnitTwoTone" | "AccessAlarm" | "AccessAlarmOutlined" | ... 9875 more ... | "ZoomOutTwoTone"'.ts(2322)
As noted, I can change the type from string to any and it will work, but I would like to know how can I fix this type error for future use. They both seem to be strings to me.
I got the implementation idea from
Rendering Material-UI icons from an array
In IGenericIconProps you specify that an iconName must be of type keyof typeof icons. This type is stricter than just string; it's looking for a specific set of string literals that are the keys of the icons object. By then having randomIcon: string in IUseGenericIconProps, you've relaxed the type to be any string and so Typescript rightfully complains. Changing it to any simply removes all type safety. Why not export that type and use it everywhere instead?
import * as icons from "#mui/icons-material";
export type IconNames = keyof typeof icons // use this in other components
interface IGenericIconProps {
iconName: IconNames;
}
export const GenericIcon = ({ iconName }: IGenericIconProps): JSX.Element => {
const Icon = icons[iconName];
return <Icon />;
};

Type 'Element[]' is missing the following properties when handling react children

I have a parent component that provides its children to a child component. The children can either be a node or an array of nodes, and depending on if it is a node or an array of nodes the children in the ChildComponent are rendered differently. However, when I a render the ChildComponent in the ParentComponent, I get the following Typescript error:
TS2786: 'ChildComponent' cannot be used as a JSX component.   Its return type 'Element | Element[]' is not a valid JSX element.     Type 'Element[]' is missing the following properties from type 'Element': type, props, key
Please see the below for the code. How can I render the ChildComponent without any Typescript errors? Thanks!
import React from 'react';
import styles from './component.scss';
interface Props {
children: React.ReactChildren;
}
const ChildComponent = ({ children }: Props): JSX.Element | JSX.Element[] => {
if (React.Children.count(children)) {
return React.Children.map(children, (child) => (
<div className={styles.childContainer}>{child}</div>
));
}
return <div className={styles.singleContainer}>{children}</div>;
};
const ParentComponent: React.FC<Props> = ({ children }) => (
<div className={styles.container}>
<ChildComponent>{children}</ChildComponent>
</div>
);
export default ParentComponent;
In your current code, the return type is JSX.Element[] from if block (incorrect) but JSX.Element (correct) from else part. That's the reason of the error you are seeing.
To fix it, you need to return single JSX expression from ChildComponent as "JSX expressions must have one parent element".
You can use a parent element like div, span or simply use React.Fragment (shorthand <></>):
const ChildComponent = ({ children }: Props) => {
if (React.Children.count(children)) {
return (
<> // HERE
{React.Children.map(children, (child) => (
<div className={styles.childContainer}>{child}</div>
))}
</>
);
}
return <div className={styles.singleContainer}>{children}</div>;
};
And children type should be ReactNode instead of ReactChildren:
interface Props {
children: React.ReactNode;
}
As these are the definitions of both the typings:
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
interface ReactChildren {
map<T, C>(children: C | C[], fn: (child: C, index: number) => T):
C extends null | undefined ? C : Array<Exclude<T, boolean | null | undefined>>;
forEach<C>(children: C | C[], fn: (child: C, index: number) => void): void;
count(children: any): number;
only<C>(children: C): C extends any[] ? never : C;
toArray(children: ReactNode | ReactNode[]): Array<Exclude<ReactNode, boolean | null | undefined>>;
}
Also, I would suggest not to use React.FC.
Here is a demo.
Edit:
PS: The else block <div className={styles.singleContainer}>{children}</div> and the if condition with count is not really required.

Styled components in a hoc react component

I am having two issues with using styled components in a hoc wrapper in react.
The component is rendered, but not with the background color.
The ComponentWithAddedColors is not valid typescript. Don't know why.
Anyone who can help with this?
interface IProps {
id: string;
left: number;
top: number;
}
export const Node: React.FC<IProps> = ({ id, left, top }) => {
return (
<Container left={left} top={top}>
{id}
</Container>
);
};
function withColors<T>(Component: React.ComponentType<T>) {
const bg = "hotpink";
const ComponentWithAddedColors = styled(Component)`
${bg && `background: ${bg};`}
`;
const result: React.FC<T> = (props) => (
<ComponentWithAddedColors {...props} />
);
return result;
}
const DraggableNode = withColors(Node);
export default DraggableNode;
I have made a code sandbox to illustrate the issue:
https://codesandbox.io/s/styled-hoc-xgduo?file=/src/Components/Node/Node.tsx
Style Errors Explained
#Mosh Feu's comment pointed me in the right direction.
You can add styles to an already styled component and you can add styles to a custom component, but those two things work differently. You have a chain that goes through both types, so things are getting lost.
When you call withColors(Node) what this is doing is passing a generated className prop to Node. But your custom component Node never does anything with this prop, so the style is never applied.
The styled method works perfectly on all of your own or any third-party component, as long as they attach the passed className prop to a DOM element.
Style Errors Fixed
If we edit Node to use this className, we get the color!
export const Node: React.FC<IProps & {className?: string}> = ({ id, left, top, className}) => {
return (
<Container left={left} top={top} className={className}>
{id}
</Container>
);
};
TS Errors Explained
As far as the typescript errors are concerned, you're getting an error about assigning your props T to the props of a styled component (ComponentWithAddedColors), which shows up as a bunch of crazy nonsense:
(props: (Pick<Pick<(PropsWithoutRef & RefAttributes<Component<T, any, any>>) | (PropsWithRef<PropsWithChildren> & {}), Exclude<...> | ... 1 more ... | Exclude<...>> & Partial<...>, Exclude<...> | ... 1 more ... | Exclude<...>> & { ...; } & { ...; }) | (Pick<...> & ... 2 more ... & { ...; })): ReactElement<...>
This is mainly because of ref forwarding through the ForwardRefExoticComponent type.
But we can work backwards to get the expected props type from the component type using a utility type:
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
So ComponentWithAddedColors has props PropsOf<typeof ComponentWithAddedColors>. We could use that, but we also know that ComponentWithAddedColors has type StyledComponent<React.ComponentType<T>, any, {}, never>, so we can go back a step further:
type StyledProps<InitialProps> = PropsOf<StyledComponent<React.ComponentType<InitialProps>, any, {}, never>>
So ComponentWithAddedColors has props StyledProps<T>.
TS Errors Fixed
That said, all of this is unnecessary, at least in the example you've shown. You are passing all of the props of ComponentWithAddedColors through to ComponentWithAddedColors, so result is the same thing as the component itself. Just return it directly.
function withColors<T>(Component: React.ComponentType<T>) {
const bg = "hotpink";
return styled(Component)`
${bg && `background: ${bg};`}
`;
}

How to create generic function Component

Is is possible to create generic function Components?
I assumed that something like next example would have done the trick:
type MyComponentProps<T> = T & {
component: ComponentType<T>,
primary?: boolean,
size?: 'S' | 'M' | 'L'
}
const MyComponent: ComponentType<MyComponentProps<T>> = ({ component, size, primary,...rest }) => {
/* ... */
}
... but not
When declaring the component as function and leaving out the React.ComponentType declaration the declaration seems to be fine:
function MyComponent<T>({ size, primary, component, ...rest }: MyComponentProps<T>) {
/* ... */
}
But writing the Component like so, TSX (I'm using latest create-react-app) does not recognize the it as a Component anymore, because it is missing the React.ComponentType declaration I assume.
This is what I finally used:
type MyComponentProps<T> = T & {
component: ComponentType<T>,
primary?: boolean,
size?: 'S' | 'M' | 'L'
}
type MyGenericComponent = <T>(p: MyComponentProps<T>) => ReactElement | null;
const MyComponent: MyGenericComponent = (props) => { ... }
Not very elegant but works. Still React does not fully recognize the function is a Component, but intellisense related to correct props check seem to work.
To my understanding, the reason it's so complicated to create generic function components with typescript seems to boil down to that typescript does not support 'higher kinded types'.

Resources