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:
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
};
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.