rony l
rony l

Reputation: 6012

Create a typescript react component that depending on a prop will render either a button or input?

I would like to have a react component that depending on an as property, which is typed as 'button' | 'input', will render either a button or an input.

Example usage of this potential component:

<div>
    <InputOrButton as="input" type="color"></InputOrButton>
    <InputOrButton as="button" type="submit"></InputOrButton>
</div>

The important part is that the component should be strongly typed. i.e. - if props.as==='input' then type="submit" will not compile.

More generally, the types of the rest of the properties for this component depend on the value of as. if as is input, it will allow all (but only) the react built-in input properties (DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>).

If as is button it will allow all (but only) the built-in button properties (DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>)

I am looking for a type-safe solution, meaning no usage of 'as' type assertions.

Here is my failed attempt (well, one of them at least):

import { ButtonHTMLAttributes, DetailedHTMLProps, InputHTMLAttributes } from 'react'

type TagName = 'button' | 'input'
type Detail<T extends TagName> = T extends 'button'
  ? DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
  : DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>

type Props<T extends TagName> = {
  as: T
} & Detail<T>

export function InputOrButton<T extends 'button' | 'input'>(props: Props<T>) {
  if (props.as === 'input') {
    return <input {...props} />
  } else {
    return <button {...props} />
  }
}

And the error i'm getting (on the input component):

Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; } | { ...; }' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.
  Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.
    Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'ClassAttributes<HTMLInputElement>'.
      Types of property 'ref' are incompatible.
        Type 'LegacyRef<HTMLButtonElement> | undefined' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.
          Type '(instance: HTMLButtonElement | null) => void' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.
            Type '(instance: HTMLButtonElement | null) => void' is not assignable to type '(instance: HTMLInputElement | null) => void'.
              Types of parameters 'instance' and 'instance' are incompatible.
                Type 'HTMLInputElement | null' is not assignable to type 'HTMLButtonElement | null'.
                  Type 'HTMLInputElement' is not assignable to type 'HTMLButtonElement' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
                    Types of property 'labels' are incompatible.
                      Type 'NodeListOf<HTMLLabelElement> | null' is not assignable to type 'NodeListOf<HTMLLabelElement>'.
                        Type 'null' is not assignable to type 'NodeListOf<HTMLLabelElement>'.ts(2322)

And the error i'm getting on the button component:

Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; } | { ...; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.
  Type '{ as: T; ref?: LegacyRef<HTMLInputElement> | undefined; key?: Key | null | undefined; accept?: string | undefined; alt?: string | undefined; autoComplete?: string | undefined; ... 282 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.
    Type '{ as: T; ref?: LegacyRef<HTMLInputElement> | undefined; key?: Key | null | undefined; accept?: string | undefined; alt?: string | undefined; autoComplete?: string | undefined; ... 282 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'ClassAttributes<HTMLButtonElement>'.
      Types of property 'ref' are incompatible.
        Type 'LegacyRef<HTMLInputElement> | undefined' is not assignable to type 'LegacyRef<HTMLButtonElement> | undefined'.
          Type '(instance: HTMLInputElement | null) => void' is not assignable to type 'LegacyRef<HTMLButtonElement> | undefined'.
            Type '(instance: HTMLInputElement | null) => void' is not assignable to type '(instance: HTMLButtonElement | null) => void'.
              Types of parameters 'instance' and 'instance' are incompatible.
                Type 'HTMLButtonElement | null' is not assignable to type 'HTMLInputElement | null'.
                  Type 'HTMLButtonElement' is missing the following properties from type 'HTMLInputElement': accept, align, alt, autocomplete, and 36 more.ts(2322)

What am i doing wrong?

Upvotes: 4

Views: 1000

Answers (1)

eroironico
eroironico

Reputation: 1372

I answered a similar question just yesterday, you can give it a look here. In that context i created a general button that could act either as a button, an anchor or an external component. In you specific case i would probably do something like:

type ValidElement<Props = any> = keyof Pick<HTMLElementTagNameMap, 'button' | 'input'>

function InputOrButton <T extends ValidElement>({ as, ...props }: { as: T } & Omit<ComponentPropsWithoutRef<T>, 'as'>): ReactElement;
function InputOrButton ({ as, ...props }: { as?: never } & ComponentPropsWithoutRef <'button'>): ReactElement;
function InputOrButton <T extends ValidElement>({
  as,
  ...props
}: { as?: T } & Omit<ComponentPropsWithoutRef<T>, "as">) {
  const Component = as ?? "button"

  return <Component {...props} />
}

And it's usage:

<>
    <InputOrButton as="button" {/* button props */} />            // a button
    <InputOrButton as="input" name="myinput" {/*input props*/} /> // an input
</>

Upvotes: 1

Related Questions