How do I type an asynchronous function that sometimes exits the process?
I want to be able to use it like this:
function useResult(result:Result): void {
// ...
}
const result: Result | undefined = await getResult();
if (result === undefined) reportError({ fatal: true });
useResult(result) // I want the type of result to be `Result` here, not `Result | undefined`
const result: Result | undefined = await getResult();
if (result === undefined) reportError({ fatal: false });
useResult(result) // I want the type of result to be `Result | undefined` here
Here's the function in question:
import { exit } from "node:process";
interface ReportableError {
fatal?: true | false | undefined;
// ...
}
export async function reportError<E extends ReportableError>(
e: E,
) { // <-- How do I type the return of this function?
// Log the error, do some async stuff...
if (e.fatal) {
// Stop running
exit();
}
// Continue running
}
CodePudding user response:
I think the only way this will work is if you make reportError()
an overloaded function with separate call signatures for the "fatal" and "non-fatal" varieties of input.
Essentially you want reportError({fatal: true})
to return the never
type because the function will never return. When you call a function whose call signature's return type is just never
, the compiler can use control flow analysis to narrow the types of variables based on reachability of code. This was implemented in microsoft/TypeScript#32695, and only triggers if the return type is explicitly annotated as just never
.
Since you want
if (result === undefined) reportError({ fatal: true });
to narrow result
from Result | undefined
to Result
, that means reportError()
must return a never
. So one call signature of reportError
should look like
function reportError(e: { fatal: true }): never;
Note that we cannot write async function reportError(e: {fatal: true}): Promise<never>
because Promise<never>
does not affect reachability the way we want... this is marked as a bug at microsoft/TypeScript#34955.
On the other hand, when you call reportError()
where fatal
is not true
, you want this to just be an asynchronous function which returns void
... well, Promise<void>
. Like this:
async function reportError(e: { fatal?: false | undefined }): Promise<void>;
And we have to separate out the fatal and non-fatal cases into their own call signatures in order for reachability analysis to work how we want. A single generic call signature that returns a conditional type like <E>(e: ReportableError) => E extends {fatal: true} ? never : Promise<void>
is conceptually the same, but is not treated like an assertion by the compiler.
Okay, so we have two call signatures and we need an implementation. The full function looks like
function reportError(e: { fatal: true }): never;
async function reportError(e: { fatal?: false | undefined }): Promise<void>;
async function reportError(e: ReportableError) {
if (e.fatal) {
throw new Error("EXITING PROCESS OR SOMETHING");
}
}
And let's see if it works how we want:
const result: Result | undefined = await getResult();
if (result === undefined) reportError({ fatal: true });
useResult(result) // okay
Great, result
is narrowed. And if we change the fatality level:
const result: Result | undefined = await getResult();
if (result === undefined) reportError({ fatal: false });
useResult(result) // error! undefined is not assignable to Result
Now result
is not narrowed, and the compiler dislikes useResult(result)
because useResult()
does not accept undefined
.
CodePudding user response:
export async function reportError<E extends ReportableError>(
e: E,
): Promise<E["fatal"] extends true ? Result : Result | undefined> {
// ...
}