Reputation: 6012
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
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