Fixing types for mapping over object to create React components - reactjs

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>

Related

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 />;
};

How to best type a TypeScript collection of React wrappers

in my team's application, we have run into a few cases where it would be nice to be able to dynamically compose component wrappers (HOCs) without having to know all the wrapper interfaces ahead of time (mostly for swapping out context providers when large portions of our component tree are rendered from different host containers).
I'm able to write a simple implementation for this, but getting optimal type safety has been... challenging 😉. In short, I'd like to be able to declare a collection of wrappers such that when any wrapper components are passed into it, the compiler will enforce that each wrapper component gets its correct props type as well.
Here's some code to illustrate the problem. Essentially, my question is how to define WrapperSet.
import * as React from "react";
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
type WrapperSet = [React.ComponentType, React.PropsWithChildren<{}>][];
👆 If I hover over WrapperSet here, TS tells me type WrapperSet = [any, any][], which is not at all protective.
Some other definitions of WrapperSet I have tried:
type WrapperSet2 = [React.ComponentType<any>, React.PropsWithChildren<any>][] &
{
[K in number]: WrapperSet2[K] extends [React.ComponentType<infer P>, any]
? [React.ComponentType<P>, React.PropsWithChildren<P>]
: never;
};
👆 TS interprets this as type WrapperSet2 = [any, any][] & { [x: number]: [any, any]; }, which is no better.
type Wrapper<P> = [React.ComponentType<P>, React.PropsWithChildren<P>];
type PropsFromComponentType<T extends React.ComponentType<any>> =
T extends React.ComponentType<infer P> ? P : never;
type WrapperSet3 = [any, any][] & {
[K in number]: WrapperSet4[K] extends [React.ComponentType<infer C>, any]
? Wrapper<PropsFromComponentType<C>>
: never;
};
👆 TS says type WrapperSet3 = [any, any][] & { [x: number]: Wrapper<unknown>; }.
It seems like there must be some way to tell TS that the outer WrapperSet array supports any kinds of Wrappers, but each Wrapper must be internally consistent, based on a single props type. Or maybe TS doesn't support this kind of expression. FYI, I am using TS 4.3.
Thanks in advance!
You can do it with a combination of types and a utility function that will enforce the props for the given component:
type ComponentAndProps<C extends React.ElementType<any>> = [C, React.ComponentPropsWithRef<C>];
type WrapperSet = ComponentAndProps< React.ElementType<any> >[];
function makeGroup<C extends React.ElementType<any>>(Component: C, props: React.ComponentPropsWithRef<C>): ComponentAndProps<C> {
return [Component, props];
}
Here's a usage example:
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
makeGroup(WrapperA, { label: "WrapperA" }),
makeGroup(WrapperB, { title: "foo" }),
makeGroup(WrapperB, {}), // Error: missing 'title' property
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
The trick is the makeGroup() helper function that allows TypeScript to infer and enforce the props type for the component.
If you just use the tuple notation then the props end up as any and TypeScript can't enforce the props:
const wrappers2: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, {}] // BAD - no error, TypeScript can't infer the props type for the component here
];
Final note - React has a number of utility types for extracting the props type from a component.
I chose ComponentPropsWithRef just in case you have a component that uses refs, but adjust as necessary:
React.ComponentProps<Component>
React.ComponentPropsWithRef<Component>
React.ComponentPropsWithoutRef<Component>
You can create type util which will accept two components and produce a tuple of wrappers and corresponding props:
import React, { FC, ComponentProps } from "react";
type Wrap<Comps extends FC<any>[]> = {
[Comp in keyof Comps]: Comps[Comp] extends React.JSXElementConstructor<any>
? [Comps[Comp], ComponentProps<Comps[Comp]>]
: never
}
type WrapperSet = Wrap<[typeof WrapperA, typeof WrapperB]>
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
}; // compiles
But there is still a problem with:
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.
This is why ComponentType expects an intersection of all props and not a union. TS is unable to figure out which prop correspond to each component in dynamic loop.
In order to fix it, we need to create extra function:
const iteration = <Comp extends React.JSXElementConstructor<any>>(
Comp: Comp,
props: ComponentProps<Comp>,
content: JSX.Element
) => <Comp {...props} >{content}</Comp>
And the whole code:
import React, { FC, ComponentProps } from "react";
type Wrap<Comps extends FC<any>[]> = {
[Comp in keyof Comps]: Comps[Comp] extends React.JSXElementConstructor<any>
? [Comps[Comp], ComponentProps<Comps[Comp]>]
: never
}
type WrapperSet = Wrap<[typeof WrapperA, typeof WrapperB]>
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
const iteration = <Comp extends React.JSXElementConstructor<any>>(
Comp: Comp,
props: ComponentProps<Comp>,
content: JSX.Element
) => <Comp {...props} >{content}</Comp>
function wrap(children: JSX.Element, wrappers: WrapperSet) {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = iteration(ComponentType, props, content)
});
return content
}
Playground

How to set default props to required props in functional components?

In Typescript, I set up a component as follows:
interface MyComponentProps {
type: 'round' | 'square';
}
const MyComponent: FC<MyComponentProps> = ({type = 'round'}) => {
return (
<div />
);
};
The type prop is required and has a default set in the component definition, but still I get and error when calling the component:
<MyComponent />
// Property 'type' is missing in type '{ }' but required in type 'MyComponentProps'.
Setting the property type to an optional type? solves the problem by implicitly changing the type to 'round' | 'square' | undefined but I don't want the property to be possibly undefined, because that would cause issues and weird code down the line where I must consider type being undefined at every point.
What do I want to happen?
I want 'type' to have a default value when not passed, but not be defined as undefined (i.e. optional).
What have I tried?
I tried adding
MyComponent.defaultProps = {
type: 'round'
};
But this didn't help at all, and also I know that defaultProps are about to become deprecated for functional components anyway.
if you want to pass default value that means value is not mandatory. if value will not come then you have default value.
I suggest you try followings this:
interface MyComponentProps {
type?: 'round' | 'square';
}
const defaultProps: MyComponentProps = {
type: 'round'
};
const MyComponent: FC<MyComponentProps> = ({type}) => {
return (
<div />
);
};
MyComponent.defaultProps = defaultProps;
Update for typescript#4.4
Since ts#4.4 you can define such a type without explicit type assertion when exactOptionalPropertyTypes flag is set:
type MyComponentProps = {
type: 'round' | 'square';
} | {
type?: never
}
const MyComponent = ({type = 'round'}: MyComponentProps) => {
return (
<div />
);
}
const NoProps = <MyComponent />
const UndefProp = <MyComponent type={undefined} /> // error
const WithProp = <MyComponent type="round" />
playground link
Unfortunatelly TS playground does not support storing exactOptionalPropertyTypes flag in the url yet. So you'll have to go into TS Config and set it manually.
I believe you cannot get what you want with straighforward typescript features. But massaging your types a bit with type assertion you can get pretty close:
interface MyComponentProps {
type: 'round' | 'square';
}
const MyComponent = (({type = 'round'}: MyComponentProps) => {
return (
<div />
);
}) as React.FC<MyComponentProps | {}>
const NoProps = <MyComponent />
const UndefProp = <MyComponent type={undefined} /> // error
const WithProp = <MyComponent type="round" />
playground link

React TypeScript component with two different prop interfaces

I want to create a React TypeScript component whose props is a union of two different interfaces. However, when I do so, I get the warning:
TS2339: Property 'color' does not exist on type 'PropsWithChildren<Props>'
How can I create a React TypeScript component with a union of two different prop interfaces and at the same time am able to destructure those props? Thanks!
sampleComponent.tsx:
import * as React from 'react';
interface SamplePropsOne {
name: string;
}
interface SamplePropsTwo {
color: string;
}
type Props = SamplePropsOne | SamplePropsTwo;
const SampleComponent: React.FC<Props> = ({ color, name }) => (
color ? (
<h1>{color}</h1>
) : (
<h1>{name}</h1>
)
);
export default SampleComponent;
Before TypeScript will let you read name or color off the union type, it needs some evidence that you're working with the correct type of Props (SamplePropsOne or SamplePropsTwo). There are a few standard ways to provide it with this.
One is by making the union a tagged union by introducing a property to distinguish branches of the union. This type checks just fine:
interface SamplePropsOne {
type: 'one';
name: string;
}
interface SamplePropsTwo {
type: 'two';
color: string;
}
type Props = SamplePropsOne | SamplePropsTwo;
const SampleComponent: React.FC<Props> = props => (
props.type === 'one' ? (
<h1>{props.name}</h1>
) : (
<h1>{props.color}</h1>
)
);
If you get the cases backwards (as I did when writing this!) then TypeScript will complain.
If the presence of a property is enough to distinguish the types, then you can use the in operator:
interface SamplePropsOne {
name: string;
}
interface SamplePropsTwo {
color: string;
}
type Props = SamplePropsOne | SamplePropsTwo;
const SampleComponent: React.FC<Props> = props => (
'color' in props ? (
<h1>{props.color}</h1>
) : (
<h1>{props.name}</h1>
)
);
If determining which type of object you have requires more complex logic, you can write a user-defined type guard. The key part is the "is" in the return type:
function isSampleOne(props: Props): props is SamplePropsOne {
return 'name' in props;
}
const SampleComponent: React.FC<Props> = props => (
isSampleOne(props) ? (
<h1>{props.name}</h1>
) : (
<h1>{props.color}</h1>
)
);
It's also worth noting that because of the way structural typing works, there's no reason props in your example couldn't have both name and color:
const el = <SampleComponent name="roses" color="red" />; // ok
If it's important to not allow this, you'll need to use some slightly fancier types:
interface SamplePropsOne {
name: string;
color?: never;
}
interface SamplePropsTwo {
color: string;
name?: never;
}
type Props = SamplePropsOne | SamplePropsTwo;
The ts-essentials library has an XOR generic that can be used to help construct exclusive unions like this.
I think what you are looking for are intersection types.
Replace this line:
type Props = SamplePropsOne | SamplePropsTwo;
with this line:
type Props = SamplePropsOne & SamplePropsTwo;
Intersection types: combine multiple interfaces/types into one
Union types: choose one of multiple interfaces/types
EDIT
What you want is not possible (i think). What you could do is destructuring every type in a single line after casting props:
const SampleComponent: React.FC<Props> = props => {
const { name } = props as SamplePropsOne;
const { color } = props as SamplePropsTwo;
return color ? <h1>{color}</h1> : <h1>{name}</h1>;
};
I think this will help
interface SamplePropsOne {
name: string;
color: never;
}
interface SamplePropsTwo {
name: never;
color: string;
}
type Props = SamplePropsOne | SamplePropsTwo;
const SampleComponent = ({ color, name }: Props) => {
console.log(color ? color : name);
}
ts playground link : https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgMpwLYAcA2EAKUA9lgM4DyIKA3gFDLIiYQBcypYUoA5gNz3IERHEShsqAN2j8AvrVqhIsRCnTY8hEqQAqAdyLI6DJhlaMIUqPwZCRY9px6z5YAJ5YUmssgC8aTLgExGSUKAA+-upBWnpE-LRCIByRgQDCRNhEVOC+yAAU1ILCogA0jMzIMmxepACUvgB8hgKJpMIQAHQi3Hm2osgA-EV2yOLMtbJAA

Typing a dynamic tag in React with TypeScript?

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>;
}

Resources