How to Merge JSX Component Props in TypeScript
October 26, 2021Let's say you have some wrapper component for input with some logic inside, and you want to accept props only for the specified input. What can you do? You can use ComponentPropsWithoutRef
(prefer ComponentPropsWithRef
, if the ref is forwarded) to mirror every prop of the specified element (tag).
type CustomInputProps = React.ComponentPropsWithoutRef<'input'>const CustomInput = (props: CustomInputProps) => {// ...return <input {...props} />}export default function App() {return (<main><CustomInput /></main>)}
Extending CustomInputProps
with a new prop is straightforward using intersections:
type CustomInputProps = React.ComponentPropsWithoutRef<'input'> & {customProp: string}const CustomInput = ({ customProp, ...props }: CustomInputProps) => {// ...return <input {...props} />}export default function App() {return (<main><CustomInput customProp="" /></main>)}
But how about replacing existing props? To give an example, what if we want to make the default name
prop required because our custom logic depends on it? We can use intersection as well:
type CustomInputProps = React.ComponentPropsWithoutRef<'input'> & {name: string}const CustomInput = ({ ...props }: CustomInputProps) => {// ...return <input {...props} />}export default function App() {return (<main><CustomInput name="" /></main>)}
But for me, it is not explicit enough. Currently, our CustomInput
component accepts more than 280 props inferred from the input
element. Can you say if the prop
you have specified (in our case name
) should be overridden or not? In some cases yes, in some no.
I would like to introduce an alternative approach.
What if we omit the specified prop and then extend it with our custom one?
type CustomInputProps = Omit<React.ComponentPropsWithoutRef<'input'>,'name'> & {name: string}const CustomInput = ({ ...props }: CustomInputProps) => {// ...return <input {...props} />}export default function App() {return (<main><CustomInput name="" /></main>)}
There is no difference in the functionality, but our type
is more explicit. We construct a type by picking all properties from elements and then removing keys we do not want.
What could we do better? I would say that some utility type with generics would be great.
export type MergeComponentProps<ElementType extends React.ElementType,Props extends object = {},> = Omit<React.ComponentPropsWithoutRef<ElementType>, keyof Props> & Propstype CustomInputProps = MergeComponentProps<'input',{name: string}>const CustomInput = ({ ...props }: CustomInputProps) => {// ...return <input {...props} />}export default function App() {return (<main><CustomInput name="" /></main>)}
Now let me explain our helper type with generics:
We've added a type variables ElementType
that extends React.ElementType
and Props
which we initialize as empty object. Then we omit Props
keys from the specified Element passed into ComponentPropsWithRef
. And the last step is applying the Props
object using intersections into our type.
I prefer using the MergeComponentProps
type because it is more explicit than a simple intersection. We can see from the first sight that we are merging props, not creating a new one, and we can be sure that the original prop was removed from the type.