Search code examples
typescriptasync-awaitassertion

Can a TypeScript value assertion be made async?


I want to encapsulate the following logic into a helper function:

if (!response.ok) {
  throw Error(await response.text());
}

so that it can read as:

await myAssert(response.ok, async () => await response.text());

But I can't figure out the right TypeScript syntax. If I do this:

async function myAssert(
  precondition: boolean,
  messageMaker: () => Promise<string | Error>
): asserts precondition
{
  const message = precondition ? "-unused-" : await messageMaker();
  assert(precondition, message);
}

then the TypeScript compiler complains:

The return type of an async function or method must be the global Promise type.

But if I do this:

async function myAssert(
  precondition: boolean,
  messageMaker: () => Promise<string | Error>
): Promise<void>
{
  const message = precondition ? "-unused-" : await messageMaker();
  assert(precondition, message);
}

then in the calling code the TypeScript compiler doesn't understand that response.ok must always be true after awaiting the assertion.

Is there a way I can have both in TypeScript?


Solution

  • It is not currently possible in TypeScript for an assertion function to be async. There's an open feature request for this at microsoft/TypeScript#37681, but for now this is not supported.

    The best you can do is to refactor so that the entire state you care about is returned by the async function. But that will turn out to be a mess if you want it to be general. You might get away with passing in a custom type guard function and assert its results:

    async function myAssert<T, U extends T>(
      state: T & (Exclude<T, U> | U),
      predicate: (t: T) => t is U,
      msgMkr: (t: Exclude<T, U>) => Promise<string | Error>
    ): Promise<U> {
      if (predicate(state)) {
        return state;
      } else {
        const msg = await msgMkr(state);
        throw (typeof msg === "string") ? new Error(msg) : msg;
      }
    }
    

    which is currently painful in TypeScript 5.4, but in TypeScript 5.5 we can expect microsoft/TypeScript#57465 to help infer type guard functions. So we could imagine taking something like this

    type Resp =
      { success: true, data: string } |
      { success: false, error(): Promise<string> };
    
    async function foo(r: Resp) {
      if (!r.success) { throw new Error(await r.error()) }
      console.log(r.data.toUpperCase());
    }
    foo({ success: true, data: "abc" }); // ABC
    foo({
      success: false, error() { return Promise.resolve("bad") }
    }).catch(e => console.log(e)); // bad
    

    and wrapping it with myAssert() as follows:

    async function fooWrapped(
      r: Resp
    ) {
      const goodR = await myAssert(r, r => r.success, r => r.error());
      //  inferred as type guard -----^^^^^^^^^^^^^^
      console.log(goodR.data.toUpperCase());
    }
    fooWrapped({ success: true, data: "abc" }); // ABC
    fooWrapped({
      success: false, error() { return Promise.resolve("bad") }
    }).catch(e => console.log(e)); // bad
    

    But it doesn't seem worth it.

    Playground link to code