Conditional types without a shared param - javascript

I want to make my props be either type A, or B. For example
export default function App() {
type Checkbox = {
type: "checkbox";
checked: boolean;
};
type Dropdown = {
type: "dropdown";
options: Array<any>;
selectedOption: number;
};
type CheckboxOrDropdown = Checkbox | Dropdown;
const Component: FC<CheckboxOrDropdown> = (props) => {
return <>"...correct component"</>;
};
// these returns are just examples
return <Component type="checkbox" checked={true} />;
return <Component type="dropdown" options={[]} selectedOption={0} />;
}
Here's a fiddle
How can I achieve the same, but without the "type" prop? So that TS recognizes the type based on other props?

You can overload your component. By overloading here I mean intersection of two functional components:
import React, { FC } from 'react'
export default function App() {
type Checkbox = {
checked: boolean;
};
type Dropdown = {
options: Array<any>;
selectedOption: number;
};
const Component: FC<Checkbox> & FC<Dropdown> = (props) => {
return <>"...correct component"</>;
};
return [<Component checked={true} />, <Component options={[]} selectedOption={0} />];
}
This is the less verbose version I know.
If you have a lot of component types and you don't want to manually intersect them, you can use distributivity.
import React, { FC } from 'react'
export default function App() {
type Checkbox = {
checked: boolean;
};
type Dropdown = {
options: Array<any>;
selectedOption: number;
};
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Overload<T> = UnionToIntersection<T extends any ? FC<T> : never>
const Component: Overload<Checkbox | Dropdown> = (props) => {
return <>"...correct component"</>;
};
return [<Component checked={true} />, <Component options={[]} selectedOption={0} />];
}
Playground

Related

Polymorphic component - selecting type from a prop in React with Typescript

This is a component I'm currently working on, named TextBody
import { HTMLAttributes } from "react";
import classNames from "classnames";
interface TextBodyProps
extends HTMLAttributes<HTMLParagraphElement | HTMLSpanElement> {
span?: boolean;
type: "s" | "m";
}
export const TextBody = ({
span,
type,
className,
children,
...props
}: TextBodyProps) => {
const textBodyClassNames = {
s: "text-body-s font-light leading-relaxed max-w-sm",
m: "text-body-m font-light leading-relaxed max-w-sm",
};
const TextBodyElement = span ? "span" : "p";
return (
<TextBodyElement
{...props}
className={classNames(textBodyClassNames[type], className)}
>
{children}
</TextBodyElement>
);
};
Is it possible to extend HTMLAttributes<HTMLSpanElement> if span prop is passed, and only HTMLAttributes<HTMLParagraphElement> if it's not, instead of having a union?
This is too long for a comment, and may be an answer. I would code it instead as follows:
import { HTMLAttributes } from "react";
import classNames from "classnames";
// This is a more accurate union: you have *either* the span
// attributes *or* the paragraph attributes, not a union of the
// attributes of both.
type TextBodyProps = (HTMLAttributes<HTMLParagraphElement> | HTMLAttributes<HTMLSpanElement>) & {
span?: boolean;
type: "s" | "m";
};
export const TextBody = ({
span,
type,
className,
children,
...props
}: TextBodyProps) => {
return span ? (
<span
{...props}
className={classNames("text-body-s font-light leading-relaxed max-w-sm", className)}
>
{children}
</span>
) : (
<p
{...props}
className={classNames("text-body-m font-light leading-relaxed max-w-sm", className)}
>
{children}
</p>
);
};
Is there a little bit of duplication? Yes. But a little duplication is better than premature abstraction, YAGNI, etc. That one in the original example was awkward to implement. Maybe there's an elegant way to have your cake and eat it too here, but I'd start with the simple easy-to-implement easy-to-read version first.
This is the final solution I've come up with:
import { ElementType } from "react";
import classNames from "classnames";
import { PolymorphicComponentProps } from "../../types";
type Variants = {
s: string;
m: string;
};
type TextBodyProps = {
type: keyof Variants;
};
const textBodyClassNames: Variants = {
s: "text-body-s font-light leading-relaxed",
m: "text-body-m font-light leading-relaxed",
};
const defaultComponent = "span";
export const TextBody = <
Component extends ElementType = typeof defaultComponent
>({
as,
type,
className,
children,
...props
}: PolymorphicComponentProps<Component, TextBodyProps>) => {
const Component = as || defaultComponent;
return (
<Component
{...props}
className={classNames(textBodyClassNames[type], className)}
>
{children}
</Component>
);
};
I think it makes it very clear how to expand it by adding a new variant, changing a variant style, or changing the default component if not specified.
PolymorphicComponentProps file contains:
import {
ComponentPropsWithoutRef,
PropsWithChildren,
ElementType,
} from "react";
type AsProp<Component extends ElementType> = {
as?: Component;
};
type PropsToOmit<
Component extends ElementType,
Props
> = keyof (AsProp<Component> & Props);
export type PolymorphicComponentProps<
Component extends ElementType,
Props = {}
> = PropsWithChildren<Props & AsProp<Component>> &
Omit<ComponentPropsWithoutRef<Component>, PropsToOmit<Component, Props>>;

Changing onClick function of React Component to React.FC

I have a Tab component which is a part of tabs structure, but I need to convert it into React.FC. Here is the original and below is what I've done so far, but I'm getting lost around the onclick functionality.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Tab extends Component {
static propTypes = {
activeTab: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};
onClick = () => {
const { label, onClick } = this.props;
onClick(label);
}
render() {
const {
onClick,
props: {
activeTab,
label
}
} = this;
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
return (
<li
className={className}
onClick={onClick}
>
{label}
</li>
);
}
}
export default Tab;
Here is my very bad attempt, which obviously is very bad
import React from 'react';
/**
* #function Tab
*/
const Tab: React.FC = () => {
type Props = {
activeTab: string;
label: string;
}
const onClick = (props) => {
const { label, onClick } = props;
onClick(label);
}
const {
onClick,
props: {
activeTab,
label
}
} = this;
let className = 'tab-list-item';
if (activeTab === label) {
className += ' tab-list-active';
}
return (
<li
className={className}
onClick={onClick}
>
{label}
</li>
);
}
export default Tab;
Any help would be much much appreciated, thank you!
If you are using typescript, you can define all the component props inside a type/interface and give it to the React.FC type, for example:
import React from 'react';
interface Props {
activeTab: string;
label: string;
onClick: (label: string) => void; // this means that the onClick param is a function that takes a label of type string as function parameter
}
// here we create a React functional component and we pass the Props interface to specify the component props
const Tab: React.FC<Props> = (props) => {
const handleOnClick = () => {
props.onClick(props.label)
}
let className = 'tab-list-item';
if (props.activeTab === props.label) {
className += 'tab-list-active';
}
return (
<li
className={className}
onClick={props.handleOnClick}
>
{props.label}
</li>
);
}
export default Tab;
If you know how to destructor an object you can clean your function in this way:
import React from 'react';
interface Props {
activeTab: string;
label: string;
onClick: (label: string) => void; // this means that the onClick param is a function that takes a label of type string as function parameter
}
// here we create a React functional component and we pass the Props interface to specify the component props
const Tab: React.FC<Props> = ({activeTab, label, onClick}) => {
const handleOnClick = () => {
onClick(label)
}
let className = 'tab-list-item';
if (props.activeTab === label) {
className += 'tab-list-active';
}
return (
<li
className={className}
onClick={handleOnClick}
>
{label}
</li>
);
}
export default Tab;

How to pass function as props to a child element in typescript

I got a parent navbar component.
const Navbar:React.FC = () => {
const classes = useStyles();
const [displayDialog, setDisplayDialog] = useState(false);
function doSomething(){
console.log('do something')
}
return (
<AppBar className={classes.background} position="static">
<Toolbar>
<Typography className={classes.title}>Labs</Typography>
<Button onClick={(event: React.MouseEvent<HTMLElement>) => setDisplayDialog(true)} className={classes.loginButton}>Login</Button>
<SettingsMenu doSomething={DoSomething}/>
//Error is here ^^
</Toolbar>
</AppBar>
);
};
export default Navbar;
What I want to do is i wanna pass dosomething as function to the child element. The main function is when clicked there will be an alert or console.log.
here is the error log
"message": "Type '{ doSomething: () => void; }' is not assignable
to type 'IntrinsicAttributes &
IntrinsicClassAttributes<Component<Pick<SettingsMenuProps, never> & {
wrappedComponentRef?: ((instance: SettingsMenu | null) => void) |
RefObject<...> | null | undefined; }, any, any>> & Readonly<...> &
Readonly<...>'.\n Property 'doSomething' does not exist on type
'IntrinsicAttributes &
IntrinsicClassAttributes<Component<Pick<SettingsMenuProps, never> & {
wrappedComponentRef?: ((instance: SettingsMenu | null) => void) |
RefObject<...> | null | undefined; }, any, any>> & Readonly<...> &
Readonly<...>'.",
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { withRouter } from 'react-router';
type SettingsMenuProps = RouteComponentProps;
type SettingsMenuState = {
settingsAnchorElement: Element | null;
currentDialog: null | 'authentication' | 'resetProgress';
showLoginButton: boolean;
};
type NavbarProps = {
doSomething: () => void
}
export class SettingsMenu extends Component<
SettingsMenuProps,
SettingsMenuState,
NavbarProps
> {
constructor(props: any) {
super(props);
this.state = {
settingsAnchorElement: null,
currentDialog: null,
showLoginButton: false
};
}
render() {
return (
<button onClick={this.props.doSomething}>Do Something</button>
);
}
}
export default withRouter(SettingsMenu);
You specified incorrect generic types in SettingsMenu definition.
Type of props should go in the first generic argument.
Try:
type SettingsMenuProps = RouteComponentProps & NavbarProps;
export class SettingsMenu extends Component<
SettingsMenuProps,
SettingsMenuState
> {
constructor(props: any) {
super(props);
this.state = {
settingsAnchorElement: null,
currentDialog: null,
showLoginButton: false
};
}
render() {
return (
<button onClick={this.props.doSomething}>Do Something</button>
);
}
}

How to reinitialize the state with react hook when rerender the components

I created a component to dispaly a question and its different options and when a user click the next button, a redirection to the same page will be executed in order to rerender the component and display a new question.
I use a checkBox component to display the question options but whenever I chose an option and go to the next question; the checkboxes are not reset for the new one (for example if I checked the second option for the first question, I get the second option checked in the second question before I checked it).
import Grid from "#material-ui/core/Grid";
import Typography from "#material-ui/core/Typography";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Dispatch } from "redux";
import { IAnswer, incrementQuestion, IQuestion, questionRequest } from "../../actions/index";
import CheckBoxWrapper from "../../components/common/CheckBoxWrapper";
import ContentQuiz from "../../components/ContentQuiz";
import history from "../../history/history";
interface IProps {
currentQuestionNumber: number;
loadingData: boolean;
questions: IQuestion[];
questionRequest: () => void;
incrementQuestion: (arg: IAnswer) => void;
numberOfQuestions: number;
}
interface IAnswerOption {
option1: boolean;
option2: boolean;
option3: boolean;
option4: boolean;
[key: string]: boolean;
}
const Quiz = (props: IProps) => {
const { currentQuestionNumber,
loadingData,
questions,
questionRequest,
incrementQuestion,
numberOfQuestions } = props;
const [answerOption, setAnswerOption] = useState<IAnswerOption>({
option1: false,
option2: false,
option3: false,
option4: false,
});
const handleChange = (option: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
setAnswerOption({ ...answerOption, [option]: event.target.checked });
};
useEffect(() => {
questionRequest();
});
const handleNextQuiz = () => {
if (currentQuestionNumber === numberOfQuestions - 1) {
history.push("/homepage");
} else {
incrementQuestion(answerOption);
history.push("/contentQuiz");
}
};
const currentQuestion = questions[currentQuestionNumber];
return (
<div>
{loadingData ? ("Loading ...") : (
< ContentQuiz
questionNumber={currentQuestionNumber + 1}
handleClick={handleNextQuiz} >
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark} >
{currentQuestion.description.replace(";", "\n")}
</SyntaxHighlighter >
<form>
<Grid container direction="column" alignItems="baseline">
{currentQuestion.options.map((option: string, index: number) => {
const fieldName = `option${index + 1}`;
return (
<Grid key={index}>
<CheckBoxWrapper
checked={answerOption[fieldName]}
value={fieldName}
onChange={handleChange(fieldName)}
label={option}
/>
</Grid>);
}
)}
</Grid>
</form>
</>
</div >
</ContentQuiz >
)}
</div>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, loadingData, questions, numberOfQuestions } = state.quiz;
return {
currentQuestionNumber,
loadingData,
questions,
numberOfQuestions
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
incrementQuestion: (answer: IAnswer) => dispatch<any>(incrementQuestion(answer)),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Quiz);
How can I reset the question options whenever I rerender the component, in order to check the options?
const fieldName = `option${index + 1}`;
Reset fieldName when you are going to the next question
I think you just need to fix your useEffect.
useEffect(() => {
setAnswerOption({
option1: false,
option2: false,
option3: false,
option4: false,
});
questionRequest();
}, []);
Also, do not forget to pass the second argument, otherwise you might have an infinite loop in your component. https://reactjs.org/docs/hooks-effect.html

Is it possible in React Typescript to use a component prop interface with another prop?

Basically, I want line injectComponentProps: object to be tied dynamically to whatever the injectComponent's prop interface is. In this case, it would be injectComponentProps: InjectedComponentProps, but I want it to be dynamic.
I.e Once prop injectComponent is set in , injectComponentProps is then defined to whatever injectComponent's components props are.
Is this possible?
interface InjectedComponentProps {
variant: string
size: number
}
const InjectedComponent: React.FC<InjectedComponentProps> = (props) => {
return <Text {...props}>hello</Text>
}
interface ComponentProps {
injectComponent: React.ReactType
injectComponentProps: object
}
const Component: React.FC<ComponentProps> = (props) => {
const InjectedComponent = props.injectComponent
return (
<>
<InjectedComponent {...props.injectComponentProps}/>
</>
)
}
const Main: React.FC = () => {
return (
<Component
injectComponent={InjectedComponent}
injectComponentProps={{ variant: 'footnote', size: 14 }}
/>
)
}
interface InjectedComponentProps {
variant: string
size: number
}
const InjectedComponent: React.FC<InjectedComponentProps> = (props) => {
return null
}
interface ComponentProps<T> {
injectComponent: React.FC<T>
injectComponentProps: T
}
const Component = <T extends {}>(props: ComponentProps<T>): JSX.Element => {
const InjectedComponent = props.injectComponent
return (
<>
<InjectedComponent {...props.injectComponentProps}/>
</>
)
}
const Main: React.FC = () => {
return (
<Component
injectComponent={InjectedComponent}
injectComponentProps={{ variant: 'footnote', size: 14 }}
/>
)
}
Instead of declaring the type simply as object, you should try modifying ComponentProps into a constrained generic component that infers the props of its type parameter.
The generic GetProps uses conditional type inference to infer props from a component, i.e., infer type parameter of a generic.
type GetProps<C extends React.ComponentType<any>> = C extends React.ComponentType<infer P> ? P : any
interface InjectedComponentProps {
variant: string
size: number
}
const InjectedComponent: React.FC<InjectedComponentProps> = (props) => {
return <Text {...props}>hello</Text>
}
// make this a generic type
interface ComponentProps<C extends React.ComponentType<any>> {
injectComponent: C
injectComponentProps: GetProps<C>
}
// and use it here
const Component: React.FC<ComponentProps<typeof InjectedComponent>> = (props) => {
const InjectedComponent = props.injectComponent
return (
<>
<InjectedComponent {...props.injectComponentProps}/>
</>
)
}
const Main: React.FC = () => {
return (
<Component
injectComponent={InjectedComponent}
injectComponentProps={{ variant: 'footnote', size: 14 }}
/>
)
}

Categories

Resources