0

Im trying convert js code to ts, аnd i have this:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then((res) => {
      return res.json().then((resJson: T) => ({
        ok: res.ok,
        status: res.status,
        body: resJson,
      }));
    })
    .then((res) => {
     if (res.ok) {
       return res.body;
     }
     return Promise.reject({
       status: res.status,
       message: res.body.message,
     });
    });
}

I dont know how to solve the problem with

message: res.body.message // Property 'message' does not exist on type 'T'

My body-response contains the "message" property as an optional only if res.ok === false. How to solve this case? Approximate usage:

type ResBody = {
  success: boolean;
  message?: string;
  data?: string[];
};

api<ResBody>("https://example.com")
  .then(({ success, data }) => {
    console.log(success, data);
  })
  .catch((err) => {
    console.log(err.message, err.status)
  });
apl-by
  • 98
  • 6
  • Can you explain your use of generics here? Specifically generic type `T`? How is `api` called? In other words, how is the type of `T` actually determined. The final `res.body` is of type `T` because you made that so in the earlier `body: resJson` and by declaring above that that `resJson` is of type `T`... But `T` has no message property because it is of unknown type! Give me details and I can solve this pretty easily. Or provide an [mre]. – Inigo Dec 06 '21 at 17:22
  • Added changes to the post – apl-by Dec 06 '21 at 17:36
  • Again, can you explain why `api` is using generics in the first place? i.e. why are you not just saying `resJson: ResBody`? There has to be a reason to use generics. The answer is important for providing the correct solution. Otherwise my answer below is a best guess / hack. – Inigo Dec 06 '21 at 17:43
  • I'm just starting to learn ts, and I took an example with generics from [here](https://stackoverflow.com/questions/41103360/how-to-use-fetch-in-typescript) – apl-by Dec 06 '21 at 17:48
  • ok, my bad. Promise is defined as generic so that you, the user, can load it with any resolve type, and then be able to reference it in the results. In that case my answer will do the trick... but I will update it in a few minutes with more details. – Inigo Dec 06 '21 at 17:59
  • I updated my answer with more explanation and another option. Good luck learning Typescript and generics! – Inigo Dec 06 '21 at 18:22
  • Thanks for clearing it up! – apl-by Dec 06 '21 at 18:30

1 Answers1

2

The Promise type has a generic parameter T so that you can declare the return type of the async function. This allows you to reference that return value in a type-safe way.

Your first then returns an object with body assigned a value of type T, and then your next then takes it as its res input. This means that res is inferred to have a body of type T. You then reference res.body.message, but no where have you declared that type T is a type with a field message.

You need to do that when you declare the generic parameter T in the function api<T> declaration as follows:

function api<T extends ResBody>(url: string): Promise<T> {
  return fetch(url)
    .then((res) => {
      return res.json().then((resJson: T) => ({
        ok: res.ok,
        status: res.status,
        body: resJson,
      }));
    })
    .then((res) => {
      if (res.ok) {
        return res.body;
      }
      return Promise.reject({
        status: res.status,
        message: res.body.message,
      });
    });
}

T extends ResBody tells Typescript that the generic type T, whatever it ends up being, will be a ResBody or a subtype of it, and thus will have an optional message field of type string.

You can be even more generic than that, i.e. if T can be any type that contains a message field as follows:

function api<T extends {message?: string}>(url: string): Promise<T> {
  return fetch(url)
    .then((res) => {
      return res.json().then((resJson: T) => ({
        ok: res.ok,
        status: res.status,
        body: resJson,
      }));
    })
    .then((res) => {
     if (res.ok) {
       return res.body;
     }
     return Promise.reject({
       status: res.status,
       message: res.body.message,
     });
    });
}

T extends {message?: string} tells Typescript that the generic type T, whatever it ends up being, has an optional message field of type string.

Inigo
  • 12,186
  • 5
  • 41
  • 70