Home > Net >  Account for two different function variations in Props
Account for two different function variations in Props

Time:03-11

I'm unsure how to properly type a function passed as props.

It can basically have two shapes:

// Hoc1.tsx
import ChildComponent from "./ChildComponent";

const Hoc1 = () => {
  const cancelResponse = async (agreement: boolean) => {
    return mockCall(agreement);
  };

  return <ChildComponent cancelResponse={cancelResponse} />;
};

export default Hoc1;

and

// Hoc2.tsx
import ChildComponent from "./ChildComponent";

const Hoc2 = () => {
  const cancelResponse = async (data: {
    agreement: boolean;
    shiftId: string;
    userId: string;
  }) => {
    return mockCall(data);
  };

  return (
    <ChildComponent
      cancelResponse={cancelResponse}
      cancelData={{ shiftId: "1", userId: "2" }}
    />
  );
};

export default Hoc2;

Now I'm unsure, how I should go about properly typing this. I tried using a conditional

These two pass different functions to the said ChildComponent

type CancelResponseVariant<ArgumentVariant> = ArgumentVariant extends boolean
  ? boolean
  : { agreement: boolean; shiftId: string; userId: string };

type Props = {
  cancelResponse: <T>(arg: CancelResponseVariant<T>) => Promise<void>;
  cancelData?: { shiftId: string; userId: string };
};

const ChildComponent = (props: Props) => {
  const { cancelData, cancelResponse } = props;
  const onCancelResponse = async (agreement: boolean) => {
    if (cancelData) {
      await cancelResponse({ agreement, ...cancelData });
    } else {
      await cancelResponse(agreement);
    }
  };

  return <button onClick={async () => onCancelResponse(true)}>Example</button>;
};

export default ChildComponent;

This seems to be nearing the solution, however is not quite it. The last call produces the following error:

Argument of type 'boolean' is not assignable to parameter of type '{ agreement: boolean; shiftId: string; userId: string; }'.ts(2345)

And in the HoC passing the function produces the following error:

Type '(agreement: boolean) => Promise' is not assignable to type '(arg: CancelResponseVariant) => Promise'. Types of parameters 'agreement' and 'arg' are incompatible. Type 'CancelResponseVariant' is not assignable to type 'boolean'. Type 'boolean | { agreement: boolean; shiftId: string; userId: string; }' is not assignable to type 'boolean'. Type '{ agreement: boolean; shiftId: string; userId: string; }' is not assignable to type 'boolean'.

Any idea how I should go about properly typing this?

Note: Obviously I tried using a simple conditional union type as well

type Data = { agreement: boolean, userId: string, shiftId: string }
type Props = {
  (agreement: boolean | Data) => Promise<void>
}

but that still throws an error inside the HoC

You can see the error reproduced here:

Edit practical-leaf-s1hx8d

or TSPlayground

PS: I am aware this is not the ideal way to pass the functions with different arg variations, but it's a 5 year old codebase that I need to migrate to typescript, so for now, I'm just working with what I can.

CodePudding user response:

Because you have cancelData in the props alongside cancelResponse, you have a discriminated union (even when the HOC doesn't provide a cancelData property), so you can define Props like this:

type Props =
    // First variant
    {
        cancelResponse: (arg: boolean) => Promise<unknown>;
        cancelData?: never;
    }
    |
    // Second variant
    {
        cancelResponse: (arg: { agreement: boolean; shiftId: string; userId: string }) => Promise<unknown>;
        cancelData: { shiftId: string; userId: string };
    };

(I'll explain about Promise<unknown> instead of Promise<void> in a moment, you probably don't need to make that change in your real code, but I did for the example.)

We need cancelData?: never; in the first variant because without it, we don't have a fully discriminated union. But we make it optional so HOCs that don't need it don't provide it (and thus use the simple (agreement: boolean) => Promise<unknown> signature).

ChildComponent figures out which call to make based on cancelData:

const ChildComponent = (props: Props) => {
    const onCancelResponse = async (agreement: boolean) => {
        if (props.cancelData) {
            await props.cancelResponse({ agreement, ...props.cancelData });
        } else {
            await props.cancelResponse(agreement);
        }
    };

    return <button onClick={async () => onCancelResponse(true)}>Example</button>;
};

That makes both Hoc1 and Hoc2 work, and disallows this incorrect Hoc3:

// With an intentional error
const Hoc3 = () => {
    const cancelResponse = async (agreement: boolean) => {
        return mockCall(agreement);
    };

    return <ChildComponent cancelResponse={cancelResponse} cancelData={{shiftId: "1", userId: "2"}} />;
    //      ^−−−−− Error as desired, the provided `cancelResponse` has the wrong signature
};

Playground link

About Promise<unknown> rather than Promise<void> in the example: That's because in Hoc1 and Hoc2, your code was doing return mockCall(agreement); and mockCall didn't have a return type annotation so ends up with Promise<unknown>. You may not need that change, if the actual calls HOCs make do have the return type Promise<void>. If they do, you're good to go with your original Promise<void>, like this.

  • Related