4

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?

4
  • I'm pretty sure they work properly in outside, but it's indistinguishable inside by if(props.as === 'input'), have you tried <input {...(props as Props<'input'>)} /> ?. Commented Dec 6, 2022 at 8:28
  • yeah you're right, it does work when doing type assertion... but I would really like to have this working without a type assertion. Commented Dec 6, 2022 at 18:41
  • OK, this seems to be the reason why the code does not compile. stackoverflow.com/questions/68897217/… Commented Dec 6, 2022 at 18:52
  • I understand, but as far as I know with my knowledge, type assertion is the only way to solve it. If you find a way to handle it without type assertion, let me know, thank you. Commented Dec 7, 2022 at 3:03

1 Answer 1

1

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
</>
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.