ecyrbe / zodios

typescript http client and server with zod validation
https://www.zodios.org/
MIT License
1.71k stars 46 forks source link

Zodios request method is returning a union of all the possible API's endpoints #292

Closed rikbrown closed 1 year ago

rikbrown commented 1 year ago

I am seeing some weird behaviour with typings, but I might be doing something stupid.

In short, I'm using makeEndpoint and makeApi to split my definitions up.

However, the return type of client.request seems to be either never (when using makeApi) or a union of all of the possible response types (when not using it). Have a look at the sandbox below and hover over the variables to see the inferred return types.

Sandbox: https://codesandbox.io/s/silent-lake-2yzxnl?file=/src/index.ts

The actual code works, it just seems to be typing.

Am I messing something up in my definitions or is something else awry?

rikbrown commented 1 year ago

The "union of all types" behaviour also seems to happen if I forgo makeEndpoint but use satisfies instead e.g.

export const getSubredditEndpoint = {
  method: "get",
  path: "r/:displayName/about",
  response,
} as const satisfies ZodiosEndpointDefinition
rikbrown commented 1 year ago

And when using apiBuilder and the spread operator like in this issue's reply's example: https://github.com/ecyrbe/zodios/issues/149#issuecomment-1255469067

So I think I'm probably doing something wrong but would love pointers.

ecyrbe commented 1 year ago

Yes, two things here:

Here is a fixed version :

import { Zodios } from "@zodios/core";
import { makeEndpoint, makeParameters, makeApi } from "@zodios/core";
import { z } from "zod";

export const subredditSchema = z.object({
  display_name: z.string(),
  icon_img: z.optional(z.nullable(z.string())),
  community_icon: z.optional(z.nullable(z.string()))
});

const subredditResponse = z.object({
  data: subredditSchema
});

const getSubredditEndpoint = makeEndpoint({
  method: "get",
  alias: 'getSubreddit',
  path: "r/:displayName/about",
  response: subredditResponse
});

const postSchema = z.object({
  permalink: z.string(),
  name: z.string(),
  title: z.string(),
  subreddit: z.string(),
  author: z.string(),
  ups: z.number(),
  likes: z.nullable(z.boolean()),
  created: z.number().transform((n) => new Date(n * 1000)),
  num_comments: z.number(),
  post_hint: z.optional(z.string()),
  url: z.optional(z.string()),
  selftext: z.optional(z.string()),
  preview: z.optional(
    z.object({
      images: z.array(
        z.object({
          resolutions: z.array(
            z.object({
              url: z.string(),
              width: z.number(),
              height: z.number()
            })
          )
        })
      )
    })
  )
});

const postResponse = z.object({
  kind: z.literal("Listing"),
  data: z.object({
    after: z.nullable(z.string()),
    before: z.nullable(z.string()),
    children: z.array(
      z.object({
        kind: z.literal("t3"),
        data: postSchema
      })
    )
  })
});

export const listPostsEndpoint = makeEndpoint({
  method: "get",
  path: ":type",
  alias: 'listPosts',
  parameters: [
    {
      name: "after",
      schema: z.optional(z.string()),
      type: "Query"
    },
    {
      name: "limit",
      schema: z.optional(z.number()),
      type: "Query"
    },
    {
      name: "type",
      type: "Path",
      schema: z.enum(["hot", "best", "new", "top", "rising"])
    }
  ],
  response: postResponse
});

const subredditListResponse = z.object({
  kind: z.literal("Listing"),
  data: z.object({
    after: z.nullable(z.string()),
    before: z.nullable(z.string()),
    children: z.array(
      z.object({
        kind: z.literal("t5"),
        data: subredditSchema
      })
    )
  })
});

export const listSubscribedSubredditsEndpoint = makeEndpoint({
  method: "get",
  path: "subreddits/mine/subscriber",
  alias: 'listSubscribedSubreddits',
  parameters: [
    {
      name: "after",
      schema: z.optional(z.string()),
      type: "Query"
    },
    {
      name: "limit",
      schema: z.optional(z.number()),
      type: "Query"
    }
  ],
  response: subredditListResponse
});

export type SubredditListingResponse = z.infer<typeof subredditListResponse>;

const withMakeApi = new Zodios(
  "https://oauth.reddit.com",
  [listPostsEndpoint, getSubredditEndpoint, listSubscribedSubredditsEndpoint],
  {
    axiosConfig: {
      headers: {
        Authorization: "Bearer foo"
      },
      params: {
        raw_json: 1
      }
    }
  }
);

async function af() {
  const rWithMakeApi = await withMakeApi.listSubscribedSubreddits({
    queries: { limit: 100, after: "" }
  });
}
ecyrbe commented 1 year ago

So i'll rename this issue as a bug for request returning a union

rikbrown commented 1 year ago

Much cleaner with aliases, thank you. I saw them mentioned in your doc but it wasn't clear exactly how to use them (didn't realise they turned into methods like that).

Thanks for the fast help. I am really enjoying using Zodios!

ecyrbe commented 1 year ago

Thank you for sponsoring zodios.

i'll update the sponsors list. Very much appeciated. This helps a lot for keeping docs website running and renewed.

ecyrbe commented 1 year ago

@rikbrown union response bug is now fixed on @zodios/core v10.7.3