0

I recently started working with TypeScript and am therefore a beginner :D.

For an application, I use the following generic method to fetch JSON objects from different endpoints:

interface UnknownObject {
  [index: string]: UnknownObject;
}

const fetchJSON = async (
  url: string,
  body?: object
): Promise<UnknownObject> => {
  try {
    let response;
    if (!body) {
      response = await fetch(url);
    } else {
      response = await fetch(url, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json"
        },
        body: JSON.stringify(body)
      });
    }
    const data = await response.json();
    if (response.status === 404) data.error = 404;
    console.log(data);
    return data;
  } catch (err) {
    throw err;
  }
};

export default fetchJSON;

I don't quite understand how I should define the interface for that method.

Currently (see code) I basically disabled Typescript with a recursive interface telling "this is an object with possible sub-objects". (I've disabled the type since it is not recommended).

Something that would fix the problem would be the possibility to define the interface as soon as I use the method in the code.

For instance, calling the method to fetch a page object could look like that: fetchJSON("pageurl"): PageProps. Without having to declare an interface when defining the function itself. I didn't find any way to do it though.

Another idea I had is to create different fetch methods based on what endpoint is used. For instance, fetchPageJSON, fetchUserJSON, etc. Like this, I would always know what object should be returned but I would have to write the same method multiple times.

What is the proper way to do this?

2
  • Do you just trust that the endpoint returns something conforming to your interface/type? Or do you perform any sort of runtime checking? Commented Jul 19, 2019 at 15:18
  • Currently, I don't check anything on the server-side. Commented Jul 20, 2019 at 6:14

1 Answer 1

3

Your UnknownObject isn't much more expressive than just unknown or even any, and since JSON.parse() in the standard library returns any, I'd say your fetchJSON should probably just return Promise<any>:

declare function fetchJSON(url: string, body?: object): Promise<any>;

I will assume you are going to trust the endpoint to return objects of a type that conforms to your expected types, and are not doing any runtime validation of the results from the endpoint. Since the compiler will not be able to verify that shape of the deserialized-from-JSON response is correct, you will need to use type assertions to tell the compiler that it is your responsibility to guarantee the type. If, of course, the endpoint returns unexpected things, then you will have lied to the compiler, the compiler will lie to the users of your library, and those users will be unhappy when their code explodes at runtime.

Let's see how to do this, with use these interfaces as an example:

interface User {
  name: string;
  age: number;
}

interface Page {
  url: string;
  body: string;
}

Well, you could, as you said, use a different fetch function for each type, assuming the url for each is distinct:

const fetchUser = async (body?: object): Promise<User> =>
  (await fetchJSON("/theUserURL", body)) as User;

const fetchPage = async (body?: object): Promise<Page> =>
  (await fetchJSON("/thePageURL", body)) as Page;

Note that you don't have to implement fetchJSON multiple times, you just have to call it. And you see the as User and as Page type assertions.


Now, if the possible values of url are constant string literals and not values that need to be computed or parsed, then you can represent the url-to-type mapping as its own interface:

interface FetchMap {
  "/theUserURL": User;
  "/thePageURL": Page;
  // ... etc
}

You never use an actual value of that type, but you can use it to define a version of fetchJSON that only allows you to pass in those known URLs:

const fetchObject: <U extends keyof FetchMap>(
  url: U,
  body?: object
) => Promise<FetchMap[U]> = fetchJSON;

const pagePromise = fetchObject("/thePageURL"); // Promise<Page>
const userPromise = fetchObject("/theUserURL"); // Promise<User>
const notAllowed = fetchObject("/aDifferentURL"); // error!

Or if you still want to allow unknown urls to be passed in, you can use a signature with a conditional type like

const fetchSomething: <U extends string>(
  url: U,
  body?: object
) => Promise<U extends keyof FetchMap ? FetchMap[U] : any> = fetchJSON;

const alsoPagePromise = fetchSomething("/thePageURL"); // Promise<Page>
const alsoUserPromise = fetchSomething("/theUserURL"); // Promise<User>
const whoKnowsPromise = fetchSomething("/aDifferentURL"); // Promise<any>

The fetchObject and fetchSomething functions are still using the equivalent of type assertions (since any can be silently converted to nay type), but they allow you to have just one function instead of multiple.


Anyway, hope that gives you some ideas; good luck!

Link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.