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

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

Related

Interface for a React Button component that can be an anchor tag or a button tag

I'm trying to create a button component that can be an anchor tag with a href or a button tag with a type prop. I have the following code:
interface IBaseProps {
children: string;
fullSize?: boolean;
theme?: 'primary' | 'secondary' | 'dark';
}
interface ILinkButtonProps extends IBaseProps {
url: string;
type: never;
props?: AnchorHTMLAttributes<HTMLAnchorElement>;
}
interface IButtonProps extends IBaseProps {
type: 'button' | 'submit' | 'reset';
url: never;
props?: ButtonHTMLAttributes<HTMLButtonElement>;
}
export const Button = ({
children,
props,
theme = 'primary',
fullSize = false,
type = 'button',
url,
}: IButtonProps | ILinkButtonProps): JSX.Element => {
const Tag: keyof JSX.IntrinsicElements = url ? 'button' : 'a';
return (
<Tag
className={`${styles.button} ${styles[theme]} ${
fullSize ? styles.fullSize : ''
}`} // not important
{...props}
{...(Tag === "button" ? {type: `${type}`} : {href: url})}
>
{children}
</Tag>
);
};
However, that gives me some typing errors, for instance:
Types of property 'onCopy' are incompatible.
Type 'ClipboardEventHandler | undefined' is not assignable to type 'ClipboardEventHandler | undefined'.
Type 'ClipboardEventHandler' is not assignable to type 'ClipboardEventHandler'.
Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLButtonElement': disabled, form, formAction, formEnctype, and 11 more.
Is there a way for me to format my code with typescript, so I can achieve a component that allows me to have both a button or a link?
Having the separate Tag variable is your downfall. Typescript is not intelligent enough to "see through" this variable and use it as a union discriminator. Once you assign to to a diff variable, the context of its meaning is lost.
You have to be quite explicit and pass the whole props into your new type guard, then consume that immediately after without assigning flags that you use later.
You will need strict null checks on for this to work. Heres a code sandbox https://codesandbox.io/s/heuristic-haslett-ovqkcw
import React from "react";
type IBaseProps = {
children: string;
fullSize?: boolean;
theme?: "primary" | "secondary" | "dark";
};
type ILinkButtonProps = IBaseProps & {
url: string;
type?: never;
props?: React.AnchorHTMLAttributes<HTMLAnchorElement>;
};
type IButtonProps = IBaseProps & {
type: "button" | "submit" | "reset";
url?: never;
props?: React.ButtonHTMLAttributes<HTMLButtonElement>;
};
export const Button = (props: ILinkButtonProps | IButtonProps): JSX.Element => {
const { children, theme, fullSize, type, url } = props;
const commonProps = {
className: `${styles.button} ${styles[theme]} ${
fullSize ? styles.fullSize : ""
}`
};
if (props.type) {
return (
<button {...commonProps} type={type} {...props.props}>
{children}
</button>
);
}
return (
<a {...commonProps} href={url} {...props.props}>
{children}
</a>
);
};
BTW, in my opinion, you shouldn't do what you are trying to do. Whilst its possible, this component likely breaks the principle of element of least surprise for the developer. Links and buttons are not semantically the same -- the dev should really make a very active choice.

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>

Why I am getting a Typescript error while using useContext with React?

I am creating a Context using useContext with TypeScript. I have a function into seperate file MovieDetailProvider.tsx as wrapper in my App.tsx file.
import { Context, MovieObject } from '../interfaces/interfaces'
export const MovieDetailContext = React.createContext<Context | {}>({})
const MovieDetailProvider: React.FC<React.ReactNode> = ({ children }) => {
const [movieData, setMovieData] = React.useState<MovieObject | {}>({})
return (
<MovieDetailContext.Provider value={{movieData, setMovieData}}>
{children}
</MovieDetailContext.Provider>
)
}
export default MovieDetailProvider
Here is Context and MovieObject Interfaces:
export interface Context {
movieData: MovieObject,
setMovieData: (movieData: MovieObject) => void
}
export interface MovieObject {
loading: boolean,
error: any,
data: any
}
Also I have 2 components which are "Consumers" using hooks useContext for exchanging data. But when trying to get context with useContext like this:
import { MovieDetailContext } from './MovieDetailProvider'
const MovieDetail: React.FC = () => {
const { movieData, setMovieData } = useContext<Context>(MovieDetailContext)
....
and
import { MovieDetailContext } from './MovieDetailProvider'
const MovieList:React.FC = () => {
const { movieData, setMovieData } = useContext<Context>(MovieDetailContext)
....
I have an error in the same place (MovieDetailContext is underlined) in two files.
The error is
TypeScript error in /home/glib/ReactCourse/graphql-project/client/src/components/MovieDetail.tsx(8,61):
Argument of type 'Context<{} | Context>' is not assignable to parameter of type 'Context<Context>'.
The types of 'Provider.propTypes' are incompatible between these types.
Type 'WeakValidationMap<ProviderProps<{} | Context>> | undefined' is not assignable to type 'WeakValidationMap<ProviderProps<Context>> | undefined'.
Type 'WeakValidationMap<ProviderProps<{} | Context>>' is not assignable to type 'WeakValidationMap<ProviderProps<Context>>'.
Types of property 'value' are incompatible.
Type 'Validator<{} | Context> | undefined' is not assignable to type 'Validator<Context> | undefined'.
Type 'Validator<{} | Context>' is not assignable to type 'Validator<Context>'.
Type '{} | Context' is not assignable to type 'Context'.
Type '{}' is not assignable to type 'Context'. TS2345
I dont have much experirnce with Typescript and I dont have an idea how to fix this Context error.
If anyone has an advice or solution please help: )
Thanks
P.S. This is my App.tsx file
imports...
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
})
const App: React.FC = () => {
return (
<ApolloProvider client={client}>
<MovieDetailProvider>
<h1>Your Movie List</h1>
<MovieList/>
<AddMovie/>
<MovieDetail/>
</MovieDetailProvider>
</ApolloProvider>
);
}
export default App;
React.useContext<Context>(MovieDetailContext) expects a parameter of type <Context> but your passing in MovieDetailContext which is of type <Context | {}>
You should remove the empty object from the context type and either set it to undefined or the object with the values set to undefined
export const MovieDetailContext = React.createContext<Context>(undefined);
Same issue with movieData when passing in the value for the Provider it's expecting MovieObject so you need set the type fro the state to a simple MOvieObject and pass in a MovieObject or undefined for initial value
const [movieData, setMovieData] = React.useState<MovieObject>(undefined)
You problaly need to adapt the type of your useContext statement to match the type of your createContext statement:
// change from
useContext<Context>(MovieDetailContext)
// to
useContext<Context | {}>(MovieDetailContext)

React Typescript Storybook - String literal types

I want to use String literal types in my component and I want to able to select any of those strings in my story using Storybook.
The component can only receive 3 types of strings: LATER | IMMEDIATELY | FCFS
But, I have this error:
Type 'string' is not assignable to type '"LATER" | "INMEDIATELY" | "FCFS"'.ts(2322) index.tsx(4, 3): The expected type comes from property 'type' which is declared here on type 'IntrinsicAttributes & LotteryCardInterface & { children?: ReactNode; }'
Component:
interface LotteryCardInterface {
type: 'LATER' | 'INMEDIATELY' | 'FCFS'
}
const LotteryCard: React.FC<LotteryCardInterface> = (props: LotteryCardInterface) =>
(<p>
{props.type}
</p>)
export default LotteryCard;
Story:
export default {
title: 'organism|LotteryCard',
component: LotteryCard,
decorators: [withKnobs]
}
const label = 'type';
const options = {
LATER: 'LATER',
INMEDIATELY: 'INMEDIATELY',
FCPS: 'FCPS',
};
const defaultValue = 'LATER';
export const Summary = () => {
const value = radios(label, options, defaultValue);
return (
<LotteryCard
type={value}
/>
)
}
Any idea how to fix it?
Thanks

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