3

Let's say I have Input component, I'd like to optionally receive one Button component, one Label and/or one Icon component. Order is not important.

// Correct use case
<Input>
 <Button />
 <Label />
 <Icon />
</Input>

// Correct use case
<Input>
 <Button />
</Input>

// Incorrect use case
<Input>
 <Button />
 <Button />
</Input>

Does anyone know what typescript interface would solve this problem?

Real use case:

            <Input
                placeholder="Username"
                name="username"
                type={InputType.TEXT}
            >
                <Input.Label htmlFor={"username"}>Label</Input.Label>
                <Input.Max onClick={() => console.log("Max clicked!")} />
                <Input.Icon type={IconType.AVATAR} />
                <Input.Button
                    onClick={onClickHandler}
                    type={ButtonType.GRADIENT}
                    loading={isLoading}
                >
                    <Icon type={IconType.ARROW_RIGHT} />
                </Input.Button>
                <Input.Status>Status Message</Input.Status>
                <Input.Currency>BTC</Input.Currency>
            </Input>

I have a dynamic check that would throw an error if anything but listed child components is being used (all of possible options are presented), but I'd like to somehow include the Typescript as well and show type errors whenever somebody uses child component of different type or duplicates a component.

Ilija Ivic
  • 107
  • 2
  • 9
  • check this as reference https://stackoverflow.com/questions/57627929/only-allow-specific-components-as-children-in-react-and-typescript – Meet Majevadiya Jun 29 '22 at 15:38
  • I cannot answer for React as I never tried, but in TS, you can build your oneOf property type. https://stackoverflow.com/questions/62591230/typescript-convert-a-tagged-union-into-an-union-type – Flavien Volken Jul 11 '22 at 11:31

1 Answers1

2

Looking at your code and question, the key factors can be regarded as these:

  • allow only one occurrence for each Button/Icon/Label component.
  • use JSX bracket hierarchy with given specific elements; use React.Children API.
  • use typescript.

These requests are contradicting at the moment. Because with typescript, it's really difficult to implement permutable array type. Here's two way of solving it, each highlights some of the key favors.

Solution 1. If the tool is not restricted to typescript, React's top level Children API can easily solve this problem.

const Button = () => {
  return <button>button</button>;
}

const Label = () => {
  return <label>label</label>
}

const Icon = () => {
  return <div>icon</div>
}

const WithSoleChild = (props) => {
  // used top-level react child API
  // https://reactjs.org/docs/react-api.html#reactchildren
  const childNames = React.Children.map(props.children, c => c.type.name);
  // count the duplication
  const childDupes = childNames.reduce((ac, cv) => ({ ...ac, [cv]: (ac[cv] || 0) + 1 }),{})
  // same child should not be duplicated
  const childValid = Object.values(childDupes).some(amount => amount < 2);
  if (!childValid) {
    // added stack trace for better dev experience.
    console.error('WithSoleChild: only one Button/Icon/Label is allowed', new Error().stack);
    // disable render when it does not fit to the condition.
    return null;
  }
  return props.children;
}

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <div>
      {/* this does not render, and throws an error to console */}
      <WithSoleChild>
        <Button />
        <Button />
      </WithSoleChild>
      {/* this does render */}
      <WithSoleChild>
        <Button />
        <Icon />
        <Label />
      </WithSoleChild>
    </div>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

<div id="root"></div>

Solution 2. However, there is a more graceful solution, which works perfectly in typescript too; it is called slot component approach, also known as containment component in the official React document.

// App.tsx
import React from "react";
// extend is necessary to prevent these types getting regarded as same type.
interface TButton extends React.FC {};
interface TIcon extends React.FC {};
interface TLabel extends React.FC {};
const Button: TButton = () => {
  return <button className="button">button</button>;
};

const Icon: TIcon = () => {
  return <div>icon</div>;
};

const Label: TLabel = () => {
  return <label>label</label>;
};
interface IWithSoleChild {
  button: React.ReactElement<TButton>;
  icon: React.ReactElement<TIcon>;
  label: React.ReactElement<TLabel>;
}

const WithSoleChild: React.FC<IWithSoleChild> = ({ button, Icon, label }) => {
  
  return null;
};

function App() {
  return (
    <div className="App">
      <WithSoleChild button={<Button />} icon={<Icon />} label={<Label />} />
    </div>
  );
}
sungryeol
  • 3,677
  • 7
  • 20
  • 1
    Thank you for the answer, I already have something similar to Solution 1 implemented. I have decided not to use approach 2, because children could have nested children as well, so API to use this input component would be pretty messy, or require each socket component to be extracted to separate variable, which would decrease readability. – Ilija Ivic Jul 07 '22 at 08:12