Typescript interface extends two component types

So this will output an <input> element - everything works perfectly:

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: FieldError;
  icon?: string;
  id: string;
  register?: UseFormRegisterReturn;
}

const StyledInputComponent = styled.input`
...
`

const MyComponent = ({
  error,
  icon,
  id,
  register,
  onChange,
  ...props
}: InputProps) => {
  return (
    <StyledInputComponent
     hasError={!!error}
     id={id}
     onChange={onChange}
     placeholder={placeholder}
     type={type}
     {...register}
     {...props}
    />
  });
};

I now need to be able to have the consuming component choose between an <input> and a <textarea>.

The problem I have is that (I think) I need to extend the interface further like this:

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLTextAreaElement> {
  error?: FieldError;
  icon?: string;
  id: string;
  inputType?: "input" | "textarea";
  register?: UseFormRegisterReturn;
}

And I'm just using the new inputType prop to switch between outputted components:

{ inputType === "input" &&  
  <StyledInputComponent
   hasError={!!error}
   id={id}
   onChange={onChange}
   placeholder={placeholder}
   type={type}
   {...register}
   {...props}
  />
}
{ inputType === "textarea" &&  
 <StyledTextAreaComponent
  hasError={!!error}
  id={id}
  onChange={onChange}
  placeholder={placeholder}
  rows={4}
  {...register}
  {...props}
 />
}

However, trying to extend the interface for both an <input> and a <textarea> has lead to numerous errors, all along these lines: errors

What's the right way to go about resolving this?


Solution 1:

Your component cannot extend the props of input AND textarea because they have they both have properties with some of the same names, but different types.

So your component should only extends one of these, depending on what is passed as the inputType.

That means you need generics here.

For example:

type InputProps<T extends 'input' | 'textarea'> = {
  id: string;
  inputType?: T;
} & JSX.IntrinsicElements[T]

Here JSX.InstrinsicElements['tagnamehere'] will return the props that react would allow for that tag name. And T is set by the value of inputType, which is either input or textarea.

Now you just need to make you component generic as well, and pass that generic to the props type:

function MyComponent<T extends 'input' | 'textarea'>(props: InputProps<T>) {
    return <></>
}

Now to test it out:

// no type errors
const testInput = <MyComponent id="123" inputType='input' checked />
const testTextarea = <MyComponent id="456" inputType='textarea' rows={10} />

// type error, can't use input prop on a textarea
const testBadProps = <MyComponent id="456" inputType='textarea' checked />
//                                                              ^ type error

Playground