Typescript Type to accept a React component type with subset of Props - javascript

There is a component Button with following props:
ButtonProps = {
variant: 'primary' | 'secondary' | 'tertiary';
label: string;
// a few more props like onChange, size etc.
}
Now, I want to create another component called "ButtonGroup" component that accepts a Button instance as a prop but can only accept primary or secondary variant.
How can I enforce that?
ButtonGroup component looks this:
<ButtonGroup
primaryButton={<Button variant="primary">Submit</Button>}
otherButton={<Button variant="secondary">Cancel</Button>}
/>
Now, the props for ButtonGroup are as follwos:
type PrimaryButtonProps = Omit<ButtonProps, 'variant'> & {
variant: 'primary' | 'secondary';
};
type ButtonGroupProps = BaseComponentProps<'div'> & {
size?: 'small' | 'medium';
primaryButton: React.ReactElement<PrimaryButtonProps, typeof Button>;
otherButton?: React.ReactElement<OtherButtonProps, typeof Button>;
};
I expect primaryButton to be a Button instance will all Button props but restricting variant to be either primary or secondary. But, with this current implementation, typescript doesn't complain if I provide a tertiary variant too.
<ButtonGroup
primaryButton={<Button variant="tertiary">Submit</Button>} // TS SHOULD COMPLAIN BUT IT DOES NOT
/>

In my opinion the cleanest solution would be to separate the implementation of each component to enforce its specific types.
interface ButtonProps {
variant: "primary" | "secondary" | "tertiary";
children?: React.ReactNode;
}
const Button = ({ variant, children }: ButtonProps): React.ReactElement => (
<button>{children}</button> // apply styles based on the variant
);
interface PrimaryButtonProps {
label: string;
variant: "primary" | "secondary";
}
const PrimaryButton = ({ label, variant }: PrimaryButtonProps) => (
<Button variant={{ variant }}>{{ label }}</Button>
);
So, when you create a ButtonGroup, you should pass the specific PrimaryButton type, instead the generic one
type ButtonGroupProps = BaseComponentProps<'div'> & {
size?: 'small' | 'medium';
primaryButton: React.ReactElement<PrimaryButtonProps, typeof PrimaryButton>;
// ...
};
<ButtonGroup
primaryButton={<PrimaryButton variant="tertiary">Submit</PrimaryButton>} // TS should complain here
/>
Hope this helps!

You can use PrimaryButton component instead of Button and it will highlight the variant other than primary or secondary.
Checkout the working CodeSandbox here.
interface ButtonProps {
variant: "primary" | "secondary" | "tertiary";
label: string;
}
function Button({ variant, label }: ButtonProps) {
return (
<button style={{ color: variant === "primary" ? "blue" : "red" }}>
{label}
</button>
);
}
type PrimaryButtonProps = Omit<ButtonProps, "variant"> & {
variant: "primary" | "secondary";
};
interface ButtonGroupProps {
primaryButton: React.ReactElement<PrimaryButtonProps>;
}
function ButtonGroup({ primaryButton }: ButtonGroupProps) {
return <div>{primaryButton}</div>;
}
// See below type assertion. It's a hack but it works :)
const PrimaryButton = Button as (props: PrimaryButtonProps) => JSX.Element;
export default function App() {
return (
<div className="App">
<ButtonGroup
primaryButton={<PrimaryButton variant="tertiary" label="Primary" />}
/>
</div>
);
}

Related

How to Type A Generic List of objects? Each object includes a different Redux Action and Selector

I need to iterate through a list of CHAT_SETTINGS and render a SelectComponent for each item.
function ParentComponent(){
const CHAT_SETTINGS = [
{
title: 'order',
choices: ['recent', 'aToZ'],
selector: orderSelector,
action: setOrderAction,
},
{
title: 'display',
choices: ['comfortable', 'compact'],
selector: displaySelector,
action: setDisplayAction,
},
];
return (
<div>
{CHAT_SETTINGS.map(setting => <SelectComponent {...setting} />)}
</div>
);
}
The SelectComponent looks more or less like this:
interface Props<T> {
title: string;
selector: (state: Store) => T;
action: ActionCreatorWithPayload<T, string>;
choices?: T[];
}
function SelectComponent<T>({ title, choices, selector, action }: Props<T>) {
const value = useSelector(selector);
const dispatch = useDispatch();
return (
<select
value={value}
onChange={(e) => {
dispatch(action(e.target.value));
}}
>
{choices.map((choice) => (
<option key={String(choice)} value={String(choice)}>
{title}
</option>
))}
</select>
);
}
✏️ A Redux Selector and a Redux Action needs to be passed in via props.
These Selectors/Actions have the following types:
displaySelector: (state: Store) => 'comfortable' | 'compact'
setDisplayAction: ActionCreatorWithPayload<'comfortable' | 'compact', string>
orderSelector: (state: Store) => 'recent' | 'aToZ'
setOrderAction: ActionCreatorWithPayload<'recent' | 'aToZ', string>
So far I was able to come up with something like this...
type ChatSettingItem<T> = {
title: string;
choices: T[];
selector: (state: Store) => T;
action: ActionCreatorWithPayload<T, string>
}
const CHAT_SETTINGS: ChatSettingItem<
ReturnType<typeof orderSelector> | ReturnType<typeof displaySelector>
>[] = [ ... ];
However, typescript keeps complaining about my actions, it says:
Type 'ActionCreatorWithPayload<"expanded" | "collapsed", string>' is not assignable to type 'ActionCreatorWithPayload<"recent" | "aToZ" | "expanded" | "collapsed", string>'.
Types of parameters 'payload' and 'payload' are incompatible.
Type '"recent" | "aToZ" | "expanded" | "collapsed"' is not assignable to type '"expanded" | "collapsed"'.
❓My question is: How can I have type-safety in this scenario?

How to fix "Property htmlFor does not exist" on Custom React element?

I have this custom component :
import React from "react";
import classnames from 'classnames';
import { ButtonVariantColor } from '../types/button';
export type IconButtonProps = {
element?: React.ElementType
icon: React.ElementType
variantColor?: ButtonVariantColor | string | undefined
} & React.HTMLAttributes<HTMLButtonElement & HTMLLabelElement>
export default function IconButton({
className,
element = 'button',
icon,
variantColor = ButtonVariantColor.default,
...props
}:IconButtonProps) {
const Icon = icon;
const ButtonElement = element;
if (!(variantColor in ButtonVariantColor)) {
variantColor = ButtonVariantColor.default;
}
return (
<ButtonElement className={ classnames('btn btn-circle border-none text-base-content', {
'bg-inherit hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700': variantColor === ButtonVariantColor.default,
'bg-blue-200 hover:bg-blue-400 dark:bg-blue-900 dark:hover:bg-blue-700': variantColor === ButtonVariantColor.info,
'bg-green-300 hover:bg-green-500 dark:bg-green-900 dark:hover:bg-green-700': variantColor === ButtonVariantColor.success,
'bg-orange-200 hover:bg-orange-400 dark:bg-orange-700 dark:hover:bg-orange-500': variantColor === ButtonVariantColor.warning,
'bg-red-200 hover:bg-red-400 dark:bg-red-900 dark:hover:bg-red-600': variantColor === ButtonVariantColor.error,
}, className) } { ...props }>
<Icon />
</ButtonElement>
);
};
and it is being used like this :
<IconButton element="label" icon={ ChevronLeftIcon } htmlFor={ menuId } />
However, htmlFor has an error and I don't know how to fix it. If the properties are inheriting from both HTMLButtonElement & HTMLLabelElement, why does it complain about htmlFor missing?
How to fix "Property htmlFor does not exist"
tl;dr - HTMLAttributes does not include htmlFor prop.
HTMLAttributes resolves to AriaAttributes, DOMAttributes and common html attributes.. AriaAttributes stands for the accessibility properties and DOMAttributes stands for common properties for DOM elements (mostly handlers). htmlFor is not a common prop, it's unique only for Label element.
Solution: you should be using HTMLLabelElement type instead.
export type IconButtonProps = {
...
} & HTMLButtonElement & HTMLLabelElement;

Changing style of Component inside NavLink

I am making a navbar, using NavLink from React-Router-Dom. Using 'isActive' prop to style the active Link, is okay.
But, how could I style the subelements inside of it.
To be exact, please see this ...
const Nav = [
{path : '/', icon : <NavIcon.Home />},
{path : '/service', icon : <NavIcon.Service />}
]
function someFunc(){
return (
Nav.map(({path, icon}) => (
<NavLink key={path} to ={path}>
{icon} //this icon is actually an svg icon and i want to change its 'fill' attr. conditionally, when the navLink is active or not. I am using .ts
</NavLink>
))}
}
const Home = () => {
<svg ....other attributes .... fill ={isActive ? 'color1' : 'color2'}> //I want this fill to be conditional so based on as to link is active or not. (**this svg exists into different component, as so do the NavLink**).
...
</svg>
}
how should I use 'isActive' from navLink inside of my svg component? and connect to 'isActive' inside of inside of entirely different component?
ps : - I TRIED making interface type in svg component as
interface Props extends NavLinkProps {
isActive : boolean;
}
const Home = ({isActive} : Props) => {
return (
<svg fill ={isActive ? 'color1' : 'color2'}>
...
</svg>
)
}
and then tried using this into my Nav.tsx component
const Nav = [
{path : '/', icon : <NavIcon.Home isActive = {'what should I put here?'} />},
{path : '/service', icon : <NavIcon.Service isActive = {} />}
]
return (
<NavLink to ={path} key ={path} className = {({isActive}) => isActive ? 'class1' : 'class2'}>
{icon}
</NavLink>
)
"My main aim is to change the 'fill color' of the svg wrapped around NavLink , based on whether the NavLink is active or not"
In addition to the NavLink component taking a function prop on the className and style props, the children prop can also take a function and is passed the isActive property.
NavLink
declare function NavLink(
props: NavLinkProps
): React.ReactElement;
interface NavLinkProps
extends Omit<
LinkProps,
"className" | "style" | "children"
> {
caseSensitive?: boolean;
children?:
| React.ReactNode
| ((props: { isActive: boolean }) => React.ReactNode);
className?:
| string
| ((props: {
isActive: boolean;
}) => string | undefined);
end?: boolean;
style?:
| React.CSSProperties
| ((props: {
isActive: boolean;
}) => React.CSSProperties);
}
Reconfigure the Nav array to instead hold a reference to the icon component you want to render and pass extraneous props to at runtime.
const nav = [
{ path: '/', end: true, icon: NavIcon.Home }, // only match when exactly "/"
{ path: '/service', icon: NavIcon.Service }
];
When mapping the nav array use the children function prop and pass the isActive property as a prop to the icon component.
nav.map(({ icon: Icon, ...props }) => (
<NavLink key={props.path} {...props}>
{({ isActive }) => <Icon isActive={isActive} />}
</NavLink>
))}

Why doesn't discriminated union work when I omit/require props?

I have a component which renders different elements based on a specific property, called type. It could have type definitions like this:
interface CommonProps {
size: 'lg' | 'md' | 'sm';
}
interface SelectInputProps extends CommonProps {
type: 'select';
options: readonly Option[];
selected: string;
}
interface TextInputProps extends CommonProps {
type: 'text';
value: string;
};
type InputProps = (SelectInputProps | TextInputProps) & ExtraProps;
function Field(props: InputProps): JSX.Element;
Now in my own component, I will access the properties of this component, like so:
import { ComponentProps } from 'react';
type FieldProps = ComponentProps<typeof Field>;
function MySpecialField(props: FieldProps) {
if (props.type === 'select') {
// this works
const { options, selected } = props;
}
return <Field {...props} />
}
This works absolutely fine. It knows in my if block that props is SelectInputProps. However, I made one small change and it appeared to completely break this mode of using the discriminated union.
type FieldProps = Omit<ComponentProps<typeof Field>, 'size'>;
In practice, here is what is happening
Why is this happening? Is there a way to fix it?
It's because the Omit<T, K> utility type does not distribute over union types in T. The implementation uses keyof T, and when T is a union, keyof T is only those keys that exist in all members of the union (keyof T is contravariant in T, so keyof (A | B) is equivalent to (keyof A) & (keyof B)). This is working as intended as per microsoft/TypeScript#31501.
Luckily, if you want a distributive version of Omit, you can write it yourself using a distributive conditional type:
type DistributiveOmit<T, K extends PropertyKey> =
T extends any ? Omit<T, K> : never;
And then you can see the change in behavior:
type MyFieldProps = DistributiveOmit<React.ComponentProps<typeof Field>, 'a'>;
/* type MyFieldProps = Omit<{
type: 'select';
options: Option[];
} & ExtraProps, "a"> | Omit<{
type: 'text';
value: string;
} & ExtraProps, "a"> */
which makes your code start working:
function MyField(props: MyFieldProps) {
if (props.type === 'select') {
const options = props.options; // okay
}
return <input />
}
Playground link to code
function MyField(props: MyFieldProps) {
if (props.type === 'select') {
// Here's the problem
const options = props.options;
}
return <input />
}

How to replace PropTypes with TypeScript Interfaces

I am using a reusable component in my React Project. That component passes some props for us to use as we like, and are required.
I am in a TypeScript file, and I am trying to convert the previous props, from PropTypes to a TS interface.
Below is the component along with the destructured Props and the interface:
The problem lies with the props: callback, actualValue, label.
interface RouteUserInfo {
username: string;
callBack: string;
actualvalue: string;
label: string;
}
export interface IState {
isLoading: boolean;
user: IUser | null;
errorMessage: HttpResponseObj | null;
}
export class UserDetailsPage extends Component<RouteComponentProps<RouteUserInfo>, IState> {
hasBeenMounted = false;
state: IState = {
errorMessage: null,
isLoading: false,
user: null
};
render(): React.ReactNode {
const { user, isLoading, errorMessage } = this.state;
const { callBack, actualValue, label, match } = this.props;
return (
<div className="container-fluid lenses-container-fluid">
{this.renderNoResourceComponent()}
<div
className="d-flex justify-content-between justify-content-center my-2"
data-test="basic-render"
>
<div className="d-flex flex-row">
<h2>{match.params.username}</h2>
<span
className="ml-3 my-2"
onChange={callBack}
value={actualValue}
placeholder={label}
data-tip
data-event="mouseover"
data-event-off="keydown mouseout"
data-for="loggedIn"
>
{user.isActive ? <ActivityBadge isActive /> : <ActivityBadge />}
</span>
</div>
{user && this.renderHeaderDropdown()}
</div>
</div>
);
}
But I get the following error, when trying to use them like that on the <span /> that says:
Type '{ children: Element; className: string; onChange: any; value: any; placeholder: any; data-tip: true; data-event: string; data-event-off: string; data-for: string; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>'.
Property 'value' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>'.ts(2322)
And on the actual props I get this:
For the callback
Property 'callBack' does not exist on type 'Readonly<{ children?: ReactNode; }> & Readonly<RouteComponentProps<RouteUserInfo, StaticContext, any>>'.ts(2339)
const callBack: any
For the actual value:
Property 'actualValue' does not exist on type 'Readonly<{ children?: ReactNode; }> & Readonly<RouteComponentProps<RouteUserInfo, StaticContext, any>>'.ts(2339)
const actualValue: any
For the label:
Property 'label' does not exist on type 'Readonly<{ children?: ReactNode; }> & Readonly<RouteComponentProps<RouteUserInfo, StaticContext, any>>'.ts(2339)
const label: any
I obviously could use :any for all this properties but I would like to know their specific types, and the methodology on how to find them.
I also provide the reusable component itself:
import React from 'react';
import ReactTooltip, { Place } from 'react-tooltip';
import './styles.scss';
export interface Props {
id: string;
place?: Place;
children?: React.ReactNode;
}
function Tooltip(props: Props): JSX.Element {
const { children, id, place, ...rest } = props;
return (
<ReactTooltip id={id} place={place} {...rest} className="tooltip">
{children}
</ReactTooltip>
);
}
export default Tooltip;
Why are you providing value, onChange and placeholder to a span ? If your span is some sort of controlled input then you should probably consider switching it for an actual <input /> element, which will not complain about these props :)

Categories

Resources