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?
if(props.as === 'input'), have you tried<input {...(props as Props<'input'>)} />?.