typescriptfetchtype-safety

Is there an easy way to statically type the resolver in wretch (using generics or otherwise) for Typescript?


I'm replacing my fetch calls with wretch and I want to type the resolver so that I can migrate cleanly and ensure type safety for my api calls.

I've discovered two ways to do it but neither are ideal.

Method 1: resolve every request with a type argument to .json()

At the top layer

import wretch from "wretch";
import FormDataAddon from "wretch/addons/formData";
import QueryStringAddon from "wretch/addons/queryString";


export const apiUntyped = wretch("api-url")
  .errorType("json")
  .addon(FormDataAddon)
  .addon(QueryStringAddon);            // no .resolve() helper

Usage

export const getUsers = async (): Promise<User[]> =>
  await apiUntyped.get("/users").json<User[]>();       // <---- add the type arg here

This works but then I have to call .json<T>() for every api call which I was hoping to avoid by using the .resolve() helper. However, that creates other problems.

Method 2: use generics to type the resolve helper

At the top layer

import wretch, { Wretch } from "wretch";
import FormData, { FormDataAddon } from "wretch/addons/formData";
import QueryString, { QueryStringAddon } from "wretch/addons/queryString";

export function apiTyped<T>(): QueryStringAddon &
  FormDataAddon &
  Wretch<FormDataAddon & QueryStringAddon, unknown, Promise<Awaited<T>>> {
  return wretch("api-url")
    .errorType("json")
    .resolve((r) => r.json<T>())              // <--- add the type arg here
    .addon(FormData)
    .addon(QueryString);
}

Usage

export const getUsers = async (): Promise<User[]> =>
  await apiTyped<User[]>().get("/users");              // no .json() call

Now I don't have to call the .json() helper but as you can see the explicit return type for the apiTyped factory is horrendous and gets worse the more addons are used. Obviously I could simply delete the explicit return type and it'll work but I'm not sure that's necessarily any better :)

What doesn't work

import wretch from "wretch";
import FormDataAddon from "wretch/addons/formData";
import QueryStringAddon from "wretch/addons/queryString";

export const apiUntypedWithResolver = wretch("api-url")
  .errorType("json")
  .resolve((r) => r.json())
  .addon(FormDataAddon)
  .addon(QueryStringAddon);

This won't work

export const getUsers = async (): Promise<User[]> =>
  await apiUntypedWithResolver.get("/users");

"unknown is not assignable to type User[]"

And this won't work

export const getUsers = async (): Promise<User[]> =>
  await apiUntypedWithResolver.get("/users").json<User[]>();

"Expected 0 type arguments but got 1".


When I was using fetch I didn't explicitly type the fetch() call and typescript simply accepted the explicit typing of Promise<User[]>. Then when I was developing downstream Typescript treated the value as a User[].


So is this how wretch was intended to be used? This is my first time using wretch so I might just be missing something. I searched the web but couldn't find any typescript examples and zero threads on this topic. The typescript documentation for wretch is a vey bare bones reference so I wasn't able to glean anything from that.

Aside from the syntax, is ensuring the static type safety here overkill? Should I add a separate step after these functions return to narrow the types so they are easier to work with downstream?

Thanks for taking a look and for any help or advice.


Solution

  • For anyone in the future who is curious, I ended up doing the type inference as part of parsing the response (with zod in this case).

    export async function getUsers(): Promise<User[]> {
      const res = await api.get("/users");
      const parsed = userSchema.array().safeParse(res);
    
      if (!parsed.success) {
        throw new Error(`Users "${res}" is not valid.`);
      }
    
      return parsed.data;
    }
    

    Happy coding!