Warm tip: This article is reproduced from stackoverflow.com, please click
typescript typescript-typings

Is it possible to annotate a function to tell the typescript complier that is has checked the type o

发布于 2020-03-27 10:20:17

I the below code snippet is it possible to annotate readText to tell the compiler it ensures this.text is string not string | undefined?

type MyResponse = {
  text: () => Promise<string>;
};

class ResponseVerfier {
  response: MyResponse;
  text?: string;

  constructor(response: MyResponse) {
    this.response = response;
  }

  async readText() {
    this.text = await this.response.text();
  }

  async verifyTextContains(value: string) {
    await this.readText();

    // I've run this.readText so I now know this.text is a string, not string | undefined
    // However I'm currently getting an error "Object is possibly 'undefined'"
    return this.text.includes(value);
  }
}

Playground link

Questioner
Henry Munro
Viewed
89
jcalz 2019-07-04 00:20

I think TypeScript is missing several features that you'd need to get this code to work so that the runtime code is the same and the compiler verifies its type safety. The first missing feature is some way to do user-defined type assertions (as suggested in this GitHub issue) in a way similar to how user-defined type guards work already. That way you could tell the compiler that this.readText() will (eventually) narrow the type of this to this & {text: string}. Even with that feature, you'd need to be able to return a Promise of the type assertion instead of the type assertion itself, which would require some thing like propagation of type predicates (as suggested in this GitHub issue). If both of those were implemented, then maybe you could do something like:

// DOES NOT COMPILE, DON'T TRY THIS
async readText(): Promise<this as (this & {text: string})> {
  this.text = await this.response.text();
}

where Promise<this as (this & {text: string})> means that readText() returns a promise that, when resolved, narrows the type of this to something whose text property is definitely a string and not just string | undefined. Alas, this doesn't work.


The way to make your code work as-is without changing anything at runtime is to use type assertions, such as the non-null assertion ! operator as shown in @Phillip's answer.

If you don't mind making changes to how your code works, then I'd strongly suggest splitting the asynchronous and synchronous code into two completely different structures, and have the asynchronous code return a fully-configured synchronous thing, like this:

type MyResponse = {
  text: () => Promise<string>;
};

// all async code should be in here
class AsyncResponseVerifier {
  constructor(public response: MyResponse) {}
  async readText(): Promise<SyncResponseVerifier> {
    return new SyncResponseVerifier(await this.response.text());
  }
}

// all code in here should be synchronous
class SyncResponseVerifier {
  constructor(public text: string) {}
  verifyTextContains(value: string) {
    return this.text.includes(value);
  }
}

And you'd use it like this:

// use the async thing to get the sync thing
async function doThings() {
  const asyncRV = new AsyncResponseVerifier({
    text: async () =>
      "an important forum raising awareness about serious things"
  });
  const syncRV = await asyncRV.readText();
  syncRV.verifyTextContains("rum raisin");
}

Link to code

Otherwise you will end up with a split-personality class where some things should be awaited and other things should not. Even if you could figure out how to make the compiler keep track of which is which (and, as I said at the top, I don't think you can), it could be hard for the developer to keep track.

Anyway, hope that helps. Good luck!