Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): added data generic to WretchResponseChain #234

Conversation

OrlovAlexei
Copy link

@OrlovAlexei OrlovAlexei commented Jun 11, 2024

Hi, @elbywan

Thank you so much for this library! Currently, I'm building an API client on top of it, and I'd like to have the ability to describe response types at the API level, as well as empower API client users to handle errors with catchers.

Here's how it currently works:

const fetchClient = wretch("api");

const api = {
  tasks: {
    getAll: (badRequest:WretchErrorCallback<unknown,unknown,undefined>) =>
      fetchClient.get("tasks").badRequest(badRequest),
  },
};

api.tasks.getAll(()=> console.error("bad request")).json<{id:number}[]>()

In this case, I cannot specify the type at the api level and have to specify it at the time of calling json(). Additionally, without changes at the api client level, I cannot handle another error using catchers.

After the proposed changes, it's expected to work as follows:

import wretch from "./src/index";

const fetchClient = wretch("api");

const api = {
  tasks: {
    getAll: () =>
      fetchClient.get<{ id: number }[]>("tasks"),
  },
};


const res = await api.tasks.getAll().badRequest(console.error).notFound(console.error).json()
//  res =  { id: number }[]

With these changes, I can specify the type at the api client level, sparing api client users from having to think about it. Additionally, it will be correctly outputted in json(). Support for catchers is included - I can handle any error without modifying the api client.

There might be other solutions to my problem, and I'd be glad to hear about them. If I missed anything, I'd be happy to add it.

@elbywan
Copy link
Owner

elbywan commented Jul 6, 2024

Hey @OrlovAlexei, thanks for the PR!

Just to let you know that I did not have enough time to review it yet as I am quite busy these days, but I am planning to have a look as soon as I can.

@elbywan
Copy link
Owner

elbywan commented Jul 17, 2024

Hey 👋 ,

Really sorry for the delay and thanks again for the PR. 🙇

I finally had a look at the code and while I think the use case is perfectly valid I am not really comfortable with adding this new generic type:

  • it applies only to .json() and I'm not sure it will work when a callback is passed (.json(x => y))
  • it would need to be propagated when calling .defer()
  • 3 generics is already a lot, I'm afraid that adding a fourth one would complexify things even more

There might be other solutions to my problem, and I'd be glad to hear about them.

Actually yes! The good news is that it is possible to write an api using the exact same syntax by taking advantage of the addon system:

import wretch, { WretchAddon, WretchResponseChain } from "wretch"

// The addon part //

// Boilerplate type definition
interface TypedJsonAddonResolver<Data> {
  // Mimics the .json() call, with an extra <Data> generic parameter
  json: <T, C extends TypedJsonAddonResolver<Data>, R, CallbackReturn = never>(
    this: C & WretchResponseChain<T, C, R>,
    callback?: (data: Data) => CallbackReturn
  ) => Promise<[CallbackReturn] extends [never] ? Data : CallbackReturn>
}

// Addon implementation
function typedJsonAddon<Data>():  WretchAddon<unknown, TypedJsonAddonResolver<Data>> {
  return {
    resolver: {
      async json(callback) {
        return this.res(res => res.json().then(json => callback ? callback(json) : json))
      }
    }
  }
}

// ------------- //

// Shape of the objects served from the jsonplaceholder website
type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const client = wretch("https://jsonplaceholder.typicode.com")
const api = {
  posts: {
    // register the addon inside the member functions and define the return types
    get: (id: number) => client.addon(typedJsonAddon<Post>()).get(`/posts/${id}`),
    getAll: () => client.addon(typedJsonAddon<Post[]>()).get("/posts")
  }
}

// 'posts' is correctly typed as an array of posts 👍
const posts = await api.posts.getAll().badRequest(console.error).notFound(console.error).json()
console.log(posts.map(post => post.title).join("\n- "))
// title is correctly typed as a string 👍
const title = await api.posts
  .get(1)
  .badRequest(console.error)
  .notFound(console.error)
  // post is correctly typed as a Post object 👍
  .json(post => post.title)
console.log(title.toLowerCase())

@OrlovAlexei
Copy link
Author

OrlovAlexei commented Aug 1, 2024

The addon system is amazing! It works great with TypeScript. Thank you so much for your response! It inspired me to understand how it works and how to design it.
Am I correct in understanding that I can add a .conflict (catcher) using the addon system?

upd:
yes :)

interface ConflictErrorCatcherAddon {
	conflict: <T, C extends ConflictErrorCatcherAddon, R>(
		this: C & WretchResponseChain<T, C, R>,
		cb: WretchErrorCallback<T, C, R>,
	) => this;
}

// Addon implementation
function conflictErrorAddon(): WretchAddon<unknown, ConflictErrorCatcherAddon> {
	return {
		resolver: {
			conflict(cb) {
				return this.error(409, cb);
			},
		},
	};
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants