Sternjobname
Sternjobname

Reputation: 779

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?

Upvotes: 0

Views: 838

Answers (2)

mtz
mtz

Reputation: 11

I've tried your solution, but I have some problems when returning an input text or a textarea

function MyComponent<T extends 'input' | 'textarea'>(props: InputProps<T>) {
  const {inputType, ...rest} = props;

  if (inputType === "input") {
    return <input
//          ^error
      type={"text"}
      {...rest}
    />
  }

  if (inputType === "textarea") {  
    return <textarea
//          ^error
      {...rest}
    />
  }
  
  return null
}

This are the returned errors:

Type '{ type: "text"; } & Omit<InputProps<T>, "inputType">' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.
  Type '{ type: "text"; } & Omit<InputProps<T>, "inputType">' is not assignable to type 'ClassAttributes<HTMLInputElement>'.
    Types of property 'ref' are incompatible.
      Type 'InputProps<T>["ref"] | undefined' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.

playground with error

I can solve the problem if the rest variable is casted to JSX.IntrinsicElements["input"] or JSX.IntrinsicElements["textarea"]... but I don't think is the best solution... Any alternative?

playground solved casting the rest variable

Upvotes: 0

Alex Wayne
Alex Wayne

Reputation: 187312

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

Upvotes: 2

Related Questions