In TypeScript, I can easily create a function with overloads that returns a different return type depending on the input. E.g.,:
function doSomething(mode: "ReturnPromise"): Promise<any>;
function doSomething(mode: "FireAndForget"): void;
function doSomething(mode: "ReturnPromise" | "FireAndForget"): Promise<any> | void {
if (mode == "ReturnPromise") {
console.log("Will return a promise")
return Promise.resolve();
} else {
console.log("Can quit as-is")
return;
}
}
doSomething("FireAndForget");
// Logs "Can quit as-is"
doSomething("ReturnPromise")
.then(() => console.log("done"))
// Logs "Will return a Promise" followed by "done"
But suppose I want to have a function that generates such a function. For example:
function generateDoer(logger: (message: string) => void):
((mode: "ReturnPromise") => Promise<any>)
| ((mode: "FireAndForget") => void) {
return (mode: "ReturnPromise" | "FireAndForget") => {
if (mode == "ReturnPromise") {
logger("Will return a promise")
return Promise.resolve();
} else {
logger("Can quit as-is")
return;
}
}
}
Conceptually, and at runtime, the function works the same as before:
const doer = generateDoer((message) => console.log(message));
doer("FireAndForget");
// Logs "Can quit as-is"
doer("ReturnPromise")
.then(() => console.log("done"))
// Logs "Will return a Promise" followed by "done"
BUT, from the TypeScript compiler perspective, the calls above are deeply unhappy. TypeScript tells me that I can't even pass FireAndForget
as the first argument:
And likewise, it doesn't think that I can call .then
on the Promise-returning one:
Any ideas on how to convince it to work after all? Thank you very much!
TypeScript Playground link here.
P.S.: Updated from a day later: jsejcksn's solution worked for me perfectly. For posterity & ease of reference: here is a diff showing the code changes required between my original problem, and the described solution:
Inside the higher-order function body: you can declare the overloaded closure using idiomatic overload and implementation signatures:
function generateDoer(logger: (message: string) => void) {
// overload signatures:
function doer(mode: "ReturnPromise"): Promise<void>;
function doer(mode: "FireAndForget"): void;
// implementation signature:
function doer(mode: "ReturnPromise" | "FireAndForget") {
if (mode == "ReturnPromise") {
logger("Will return a promise");
return Promise.resolve();
} else {
logger("Can quit as-is");
return;
}
}
// return the closure:
return doer;
}
…and to describe these overload signatures in an explicit return type annotation, you can simply copy them as call signatures inside braces:
type Doer = {
(mode: "ReturnPromise"): Promise<void>;
(mode: "FireAndForget"): void;
};
function generateDoer(logger: (message: string) => void): Doer {
// …implementation from above
}
You might not encounter this syntax often, but it might help to think of the call signatures as resembling methods without property names — they're intrinsic to the type itself.
It's not necessary to define the return type annotation as a named alias — you can write it inline. All together that looks like this:
function generateDoer(logger: (message: string) => void): {
(mode: "ReturnPromise"): Promise<void>;
(mode: "FireAndForget"): void;
} {
function doer(mode: "ReturnPromise"): Promise<void>;
function doer(mode: "FireAndForget"): void;
function doer(mode: "ReturnPromise" | "FireAndForget") {
if (mode == "ReturnPromise") {
logger("Will return a promise");
return Promise.resolve();
} else {
logger("Can quit as-is");
return;
}
}
return doer;
}