Skip to content
Advertisement

How to define React component with conditional props interface?

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)

Playground here

How can i define such component?

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} />;
}

Playground #1

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}/>
}

Playground #2

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
/>
Advertisement