reactjs - Betinget prop-type basert på prop-verdi


Jeg prøver å lage et TextField-element som bruker enten input eller textarea avhengig av verdien til multiline-prop.

Jeg sliter med å betinge tildelingen av elementets prop-type av verdien til multiline.

For eksempel gir koden nedenfor meg en feil

'multiline' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

Er det en måte å gjøre det på?

const input = "input";
const textarea = "textarea";

export type TextFieldMultiline<Multiline extends boolean> = {
 multiline: Multiline,
}

export type TextFieldProps<Multiline extends boolean> = 
TextFieldMultiline<Multiline> & (Multiline extends true ? React.ComponentPropsWithRef<typeof textarea> :
React.ComponentPropsWithRef<typeof input>)

export const TextField = ({multiline, ...props}:  TextFieldProps<typeof multiline>) => 
<> {multiline ? <input {...props}/> : <textarea {...props}/>}</>

Svar #1

I stedet for å bruke en generell betinget type for propstypen din, bruk en diskriminert union:

type ReactTextAreaProps = React.ComponentPropsWithRef<"textarea">;
type ReactInputProps = React.ComponentPropsWithRef<"input">;

export type TextFieldMultiline =
    | ({ multiline: true } & ReactTextAreaProps)
    | ({ multiline: false } & ReactInputProps);

Da ser komponenten slik ut:

export const TextField = (props: TextFieldMultiline) => {
    if (props.multiline) {
        const { multiline: _, ...rest } = props;
        return <textarea {...rest} />;
    }
    const { multiline: _, ...rest } = props;
    return <input {...rest} />;
};

Brukseksempler:

// Valid, allows `textarea`-specific `rows` prop, since `multiline` is `true`:
const a = <TextField multiline={true} rows={10} />;

// Valid, allows `input`-specific `pattern` prop, since `multiline` is `false`:
const b = <TextField multiline={false} pattern="\d+" />;

// INVALID, `multiline` is `true` and `textarea` doesn't have `pattern`:
const c = <TextField multiline={true} pattern="\d+" />;
//                                    ^^^^^^^−−− Error: Type '{ multiline: true; pattern: string; }' is not assignable to type 'IntrinsicAttributes & TextFieldMultiline'. Property 'pattern' does not exist on type...

// INVALID, `multiline` is `false` and `input` doesn't have `rows`:
const d = <TextField multiline={false} rows={10} />;
//                                     ^^^^−−−−− Error: Type '{ multiline: false; rows: number; }' is not assignable to type 'IntrinsicAttributes & TextFieldMultiline'. Property 'rows' does not exist on type...

Lekeplass

Ved første øyekast ville du tro at du kunne flytte destruktureringen til parameterlisten eller toppnivået av komponentfunksjonen (som i praksis er det samme), men TypeScripts flow-analyse kan ikke spore at multiline som er sann, innsnevrer typen til rest når du gjør det (playground). Med det ovenfor kan den det.


Jeg ville ikke gjort det, men hvis du virkelig ønsket å unngå den gjentatte destruktureringen, kunne du bruke typepåstander slik:

export const TextField = ({ multiline, ...rest }: TextFieldMultiline) => {
    return multiline
        ? <textarea {...rest as ReactTextAreaProps} />
        : <input {...rest as ReactInputProps} />;
};

(Merk at det ikke er nødvendig med fragment-innpakningen i spørsmålet.)

Jeg foretrekker vanligvis løsninger uten typepåstander ettersom de er mindre skjøre (eller rettere sagt, de er mindre stille skjøre — når de bryter, lager de lyd om det 😊), men det er en vurderingssak uansett.


Til slutt, hvis du hadde en grunn til at du ikke kunne endre propstypen din til en diskriminert union og måtte holde deg til den generiske betingede typen, kunne du gjøre komponentfunksjonen din generisk for å unngå det sirkulære problemet med typen til multiline. Men i så fall tror jeg ikke du kan unngå typepåstandene i implementeringen:

type ReactTextAreaProps = React.ComponentPropsWithRef<"textarea">;
type ReactInputProps = React.ComponentPropsWithRef<"input">;

export type TextFieldMultiline<Multiline extends boolean> = {
    multiline: Multiline;
};

export type TextFieldProps<Multiline extends boolean> =
    TextFieldMultiline<Multiline> &
        (Multiline extends true
            ? ReactTextAreaProps
            : ReactInputProps);

export const TextField = <Multiline extends boolean>({
    multiline,
    ...props
}: TextFieldProps<Multiline>) => {
    return multiline
        ? <textarea {...props as ReactTextAreaProps} />
        : <input {...props as ReactInputProps} />;
};

Lekeplass

Relaterte artikler