i need to define “Field” component that renders textarea
or input
depends on prop multiline
I trying to do this like that:
import React from 'react'; type Props<T extends boolean = boolean> = { multiline: T } & T extends true ? React.HTMLProps<HTMLTextAreaElement> : React.HTMLProps<HTMLInputElement> export const Field: React.FC<Props> = ({ multiline, ...props }) => { // error here const Element = multiline ? 'textarea' : 'input'; return <Element {...props} onInput={e => {}} />; // error here } // usage const result = ( <Field onChange={e => console.log(e.target.value)} /> // error here );
But typescript provide several errors like:
1 Property 'multiline' does not exist on type 'HTMLProps<HTMLInputElement> & { children?: ReactNode; }'.(2339) 2 [large error, more in playground] 3 Property 'value' does not exist on type 'EventTarget'.(2339)
How can i define such component?
Advertisement
Answer
Problem: No T
in Field
You have defined a generic type Props
that depends on T
but your component is not generic. It always takes Props<boolean>
which resolves to the HTMLInputElement
props because boolean extends true
is false
. The reason {multiline: boolean}
is getting lost is because you need parentheses around the rest of your type.
React.HTMLProps
When using your React.HTMLProps
typings I didn’t get errors when assigning mismatched properties like type="number"
to a textarea
or rows={5}
to an input
. The more restrictive types are JSX.IntrinsicElements['textarea']
and JSX.IntrinsicElements['input']
(which resolve to a type like React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>
). If you want strict enforcement then use these! This also makes the e
value in the onChange
callback get the correct type based on the element.
Implementation
When using a generic component with restrictive types, we now get an error in the implementation on return <Element {...props} />;
I thought that breaking it into two (return multiline ? <textarea {...props} /> : <input {...props}/>;
) would help but we still get errors. Conditionals are rough. You can use as
assertions to fix things. I’m generally ok with making assertions in the implementation of a function when the usage of it stays strictly typed. So you can do this:
type Props<T extends boolean = boolean> = { multiline: T } & (T extends true ? JSX.IntrinsicElements['textarea'] : JSX.IntrinsicElements['input']) export const Field = <T extends boolean>({ multiline, ...props }: Props<T>) => { const Element = multiline ? 'textarea' : 'input'; return <Element {...props as any} />; }
Union Type
We can avoid having to make assertions by typing Props
as a union of two situations. This allows us to check which type in the union we have by looking at props.multiline
. This gets messy though because you cannot desctructure until after you have discriminated the union, but we don’t want to pass multiline through to the DOM.
This code passes all type checks, but it needs additional work to prevent passing multiline
through to the DOM.
type Props = ( { multiline: true } & JSX.IntrinsicElements['textarea'] | { multiline: false } & JSX.IntrinsicElements['input'] ); export const Field = ({ ...props }: Props) => { return props.multiline ? <textarea {...props} /> : <input {...props}/> }
Usage
Either way the usage is very strongly typed! Our onChange
callback gets the correct type like React.ChangeEvent<HTMLTextAreaElement>
and we get error if passing textarea
props when multiline={false}
or vice-versa.
<Field onChange={e => console.log(e.target.value)} // e: React.ChangeEvent<HTMLTextAreaElement> multiline={true} rows={5} // ok type="number" // error /> <Field onChange={e => console.log(e.target.value)} // e: React.ChangeEvent<HTMLInputElement> multiline={false} type="number" // ok rows={5} // error />