Home > Mobile >  Typescript discriminated union with intersection
Typescript discriminated union with intersection

Time:07-23

Example sandbox

I have a type

type TFormFieldFileProps = {
    componentProps: TFileUploaderProps;
    select?: never;
    checkbox?: never;
    file: true;
};

type TFormFieldSelectProps = {
    componentProps: TCustomSelectProps;
    select: true;
    checkbox?: never;
    file?: never;
};

type TFormFieldCheckboxProps = {
    componentProps: TCustomCheckboxProps;
    select?: never;
    checkbox: true;
    file?: never;
};

type TFormFieldInputProps = {
    componentProps: TCustomInputProps;
    select?: never;
    checkbox?: never;
    file?: never;
};

export type TFormFieldProps = { boxProps?: BoxProps } & (
    | TFormFieldCheckboxProps
    | TFormFieldInputProps
    | TFormFieldSelectProps
    | TFormFieldFileProps
);

I want to remove componentProps prop and instead set each type to be an intersection of componentProps prop type and the other select checkbox file type.

type TFormFieldFileProps = TFileUploaderProps & {
    select?: never;
    checkbox?: never;
    file: true;
};

type TFormFieldSelectProps = TCustomSelectProps & {
    select: true;
    checkbox?: never;
    file?: never;
};

type TFormFieldCheckboxProps = TCustomCheckboxProps & {
    select?: never;
    checkbox: true;
    file?: never;
};

type TFormFieldInputProps = TCustomInputProps & {
    select?: never;
    checkbox?: never;
    file?: never;
};

export type TFormFieldProps = { boxProps?: BoxProps } & (
    | TFormFieldCheckboxProps
    | TFormFieldInputProps
    | TFormFieldSelectProps
    | TFormFieldFileProps
);

But it doesn't work.

const FormField = (props: TFormFieldProps) => {
    const { select, checkbox, file, boxProps, ...rest } = props;

    return (
        <Box
            {...boxProps}
            sx={{ '& > *': { width: 1 } }}
        >
            {select ? (
                // error: missing some property from TFormFieldCheckboxProps
                <CustomSelect {...rest} />
            ) : checkbox ? (
                // error: missing some property from TFormFieldInputProps
                <CustomCheckbox {...rest} />
            ) : file ? (
                // error: missing some property from ...
                <FileUploader {...rest} />
            ) : (
                // error: missing some property from ...
                <CustomInput {...rest} />
            )}
        </Box>
    );
};

I understand why it doesn't work but I don't understand how to solve this problem without having to specify each property on each type...

Can I make it work without writing all the props from all the types in all discriminated union types? If so, how?

CodePudding user response:

For clarity, the issue here is that, while TypeScript 4.6 and above supports control flow analysis on destructured discriminated unions, this does not work for rest properties (as of TypeScript 4.7).

So, this works:

interface Foo { type: "foo"; rest: { x: string } }
interface Bar { type: "bar"; rest: { y: number } }

const process = ({ type, rest }: Foo | Bar) =>
  type === "foo" ? rest.x : rest.y; // okay

but this fails:

interface Foo { type: "foo"; x: string }
interface Bar { type: "bar"; y: number }

const process = ({ type, ...rest }: Foo | Bar) =>
  type === "foo" ? rest.x : rest.y; // errors
// -------------------> ~ -----> ~
// Property does not exist on {x: string} | {y: number}

There's a recent open request at microsoft/TypeScript#46680 to support this, but it hasn't been implemented yet. You might want to give that issue a

  • Related