'Typescript conditional return type based on props
- In electron application I have a component that renders a button and sends a message to an arbitrary channel to the
mainprocess. - The
mainprocess does some work and returns back the result, its return type is depended on the channel name. - Parent component also passes a callback along with channel name.
const Button = ({ name, channel, callback }: ButtonProps) => {
const [isDisabled, setDisabled] = useState<boolean>(false);
const onClickHandler = () => {
setDisabled(true);
window.electron.ipcRenderer.sendMessage(channel);
};
useEffect(() => {
// type definition for the function below:
// on<T>(channel: string, func: (result: T) => void): () => void;
window.electron.ipcRenderer.on<ReturnType>(channel, (result) => {
setDisabled(false);
callback(result);
});
return () => {
window.electron.ipcRenderer.removeAllListeners(channel);
};
}, [channel, callback]);
return <button
type="button"
onClick={onClickHandler}
disabled={isDisabled}
>
{name}
</button>;
};
export default Button;
How do I specify a type of a result that is being passed to a callback?
I started with something like this:
type ReturnType = OpenDialogReturnValue | SaveDialogReturnValue;
type ButtonProps = {
name: string;
} & (
| {
channel: "OPEN_DIALOG";
callback: (result: OpenDialogReturnValue) => void;
}
| {
channel: "SAVE_EXAMPLE";
callback: (result: SaveDialogReturnValue) => void;
}
);
But it throws type error on the line with callback(result);
Argument of type 'ReturnType' is not assignable to parameter of type 'OpenDialogReturnValue & SaveDialogReturnValue'.
What is wrong here?
Solution 1:[1]
There are a number of ways you can tackle this issue.
The core problem
The reason why you are seeing that error is because even though you had separated the differentiating values for channel and callback properties into two respectively different objects, channel and callback types are still undetermined when they are within the useEffect function. This means that TypeScript does not know what parameter type the callback should have and will automatically request an intersection of both
OpenDialogReturnValue and SaveDialogReturnValue types just to be sure that whatever the callback is in runtime - it will definitely get an object argument that it can use.
Solutions
- The simplest way to fix this is to lend TypeScript a hand and tell it to trust us by using type casting. As we already know, TypeScript is asking us to provide an argument for
callbackwith an intersection type as it is not sure whichcallbackfunction will exist in runtime((result: OpenDialogReturnValue) => void) | (result: SaveDialogReturnValue) => void)). We can instead tell TypeScript that there is only onecallbackand it is the argument types that vary(results: OpenDialogReturnValue | SaveDialogReturnValue) => void. This can be done by first extracting the possible parameter types thatcallbackcan have by using a Utility type calledParametersand then casting its first parameter overcallbackto assign a new variable calledtypedCallback:
useEffect(() => {
type CallbackParameterTypes = Parameters<typeof callback>;
const typedCallback = callback as (results: CallbackParameterTypes[0]) => void;
window.electron.ipcRenderer.on<CallbackParameterTypes[0]>(channel, (result) => {
setDisabled(false);
typedCallback(result);
});
- If the association between
channelandcallbackparameter types do not matter, then the above can be simplified into the following:
type ButtonProps = {
name: string;
channel: "OPEN_DIALOG" | "SAVE_EXAMPLE";
callback: (result: OpenDialogReturnValue | SaveDialogReturnValue) => void;
};
useEffect(() => {
window.electron.ipcRenderer.on<Parameters<typeof callback>[0]>(channel, (result) => {
setDisabled(false);
callback(result);
});
- Another way you can approach the problem is by using type predicates which can be difficult to maintain, however they let TypeScript know exactly what type is being used when. To apply them, you would first need to refactor the union objects into separate interfaces; e.g.
OpenDialogObjectandSaveExampleObject. You can then use anistype predicate to tell TypeScript which particular interface does the currentpropsobject adhere to as done in theisOpenDialogfunction. The function itself uses thechannelproperty to decide which type the object is. This can then be used withinuseEffectto help determine what parameter typeprops.callbackis going to have during runtime. However as you can observe - the syntax can get a little bit repetitive, especially if you will have more types. For completeness; the type predicate function will only produce wanted results when passing and inferring the fullpropsobject and not individualchannelandcallbackproperties. The reason for that is because these property types are not enough to determine what type the overall object will be:
interface OpenDialogObject {
channel: "OPEN_DIALOG";
callback: (result: OpenDialogReturnValue) => void;
}
interface SaveExampleObject {
channel: "SAVE_EXAMPLE";
callback: (result: SaveDialogReturnValue) => void;
}
type ButtonProps = {name: string;} & (OpenDialogObject | SaveExampleObject);
if (isOpenDialog(props)) {
window.electron.ipcRenderer.on<Parameters<typeof callback>[0]>(channel, (result) => {
setDisabled(false);
callback(result);
});
} else {
window.electron.ipcRenderer.on<Parameters<typeof callback>[0]>(channel, (result) => {
setDisabled(false);
callback(result);
});
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
