如何在打字稿中键入异步的潜在进程退出函数?

How to type an asynchronous potentially process-exiting function in typescript?

如何键入有时会退出进程的异步函数?
我希望能够像这样使用它:

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

这里是有问题的函数:

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
}
export async function reportError<E extends ReportableError>(
    e: E,
): E["fatal"] extends true ? never : Promise<Result | undefined> {
  // ...
}

conditional types

我认为唯一可行的方法是,如果您将 reportError() 设为 overloaded function,并为“fatal”和“non-fatal”输入类型分别设置调用签名。


本质上你想要 reportError({fatal: true}) 到 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,并且只有在 return 类型被明确注释为 never.[=45 时才会触发=]

既然你想要

if (result === undefined) reportError({ fatal: true });

resultResult | undefined缩小到Result,也就是说reportError()必须return一个never。所以 reportError 的一个调用签名应该看起来像

function reportError(e: { fatal: true }): never;

请注意,我们不能写 async function reportError(e: {fatal: true}): Promise<never>,因为 Promise<never> 不会以我们想要的方式影响可达性...这在 microsoft/TypeScript#34955.

处被标记为错误

另一方面,当您调用 reportError()fatal 不是 true,您希望它只是一个异步函数 returns void...好吧,Promise<void>。像这样:

async function reportError(e: { fatal?: false | undefined }): Promise<void>;

而且我们必须将致命案例和 non-fatal 案例分离到它们自己的调用签名中,以便可达性分析按我们想要的方式工作。与 <E>(e: ReportableError) => E extends {fatal: true} ? never : Promise<void> 类似的单个 generic call signature that returns a conditional type 在概念上是相同的,但编译器不会将其视为断言。


好的,我们有两个调用签名,我们需要一个实现。完整的功能看起来像

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");
  }
}

让我们看看它是否按照我们想要的方式工作:

const result: Result | undefined = await getResult();
if (result === undefined) reportError({ fatal: true });
useResult(result) // okay

太好了,result缩小了。如果我们改变死亡率水平:

const result: Result | undefined = await getResult();
if (result === undefined) reportError({ fatal: false });
useResult(result) // error! undefined is not assignable to Result

现在result没有变窄,编译器不喜欢useResult(result)因为useResult()不接受undefined.

Playground link to code