ecyrbe / zodios

typescript http client and server with zod validation

Home Page:https://www.zodios.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Stronger typing on `makeApi`

andenacitelli opened this issue · comments

commented

Just took me about twenty minutes to figure out what was wrong with this:

const commonParameters = parametersBuilder()
  .addHeader("authorization", z.string().min(1))
  .addHeader("email", z.string().email());

const usersApi = makeApi([
  {
    method: "post",
    path: "/",
    response: z.undefined(),
    params: commonParameters.build(),
  },
  {
    method: "get",
    path: "/",
    response: UserSchema,
    params: commonParameters.build(),
  },
  {
    method: "put",
    path: "/",
    response: UserSchema,
    params: commonParameters.addBody(UserCreateInputSchema).build(),
  },
  {
    method: "delete",
    path: "/",
    response: z.undefined(),
    params: commonParameters.build(),
  },
]);

The issue is that params is supposed to be parameters. I didn't have any kind of error pop up in my editor. Is it possible to more strongly type this so that it gives you an error in your editor, or does TypeScript just not allow this? Or is something in my editor just messed up?

And sidenote, love the project! Coupled with zod-prisma-types, this is a much quicker-to-prototype alternative to OpenAPI that also doesn't require a code generation step and tends to integrate a bit more smoothly with Prisma. Has the same disadvantage as tRPC where it couples you to TypeScript, but I honestly feel like that's an advantage for smaller projects.

commented

Hello @aacitelli,

This is due to typescript allowing unknown properties on functions (function matching is contravariant, while here we would like it to be covariant), check this:
image

commented

in zodios v11 you'll be able to use as const and satisfies together as a workaround:
image

commented

If anyone has a solution to this, i'll keep this open in case someone has a working idea

Thanks to this issue, it solved a problem I have been having with params instead of parameters and using parametersBuilder() and when using that const, using .build() when setting it on the apiBuilder parameter member.

Example below:

const movieParams = parametersBuilder().addParameters('Query', {
  limit: z.number().optional().default(1000),
  offset: z.number().optional().default(0),
  page: z.number().optional().default(1),
  _id: z.string().optional(),
});

export const movieApi = apiBuilder({
  method: 'get',
  path: '/movie',
  alias: 'getMovies',
  description: 'Get all movies',
  parameters: movieParams.build(),
  response: movieResponse,
}).build();

This got the type signatures to finally calm down and not keep giving me TS errors.

commented

edit: this doesn't work, nevermind
https://tsplay.dev/WYllxm

interface Endpoint {
  method: "get" | "post" | "put" | "patch" | "delete";
  path: string;
  response: z.ZodTypeAny;
  parameters?: Array<{
    name: string;
    type: "Query" | "Path" | "Body" | "Header";
    schema: z.ZodTypeAny;
  }>;
};
function makeEndpoint<T extends Endpoint>(api: {[K in keyof Endpoint]: T[K]}) {
  return api;
}
function myMakeApi<const T extends unknown[]>(api: { [K in keyof T]: ZodiosEndpointDefinition<T[K]> }) {
  return api
}

seems to be able to do the thing (if I understood the problem correctly)
If you ever need extra things on Endpoint you may extend the interface anyways

commented
declare function makeApi<Api extends ZodiosEndpointDefinitions>(
    api: Narrow<Api>
        // & NoInfer<
            & readonly ZodiosEndpointDefinition<any>[]
            & readonly {[K in keyof Api[0] as K extends X ? never: K]: never}[]
        // >
    ): Api;
type X = Extract<keyof ZodiosEndpointDefinition, string>
type NoInfer<T> = [T][T extends any ? 0 : never]
interface ZodiosEndpointDefinition<R = unknown> { /*...*/ } // to make it extandable for custom keys

is a minimum requirement to have key autocompletion and key exclusion
Problems: it's not [A, B], is't (A | B)[]

edit: const in template is not needed, forgot to remove after testing it

commented

there are some tricks out there to do some kind of strict type matching, but i failed to make them work with type narrowing + tuples.
My only hope is that typescript will add a statisfies keyword for generics like they did for as const.
So far the opened issue for this has a negative feedback from typescript team that don't want to implement it.
We need to change their mind somehow

commented
type UpTo<N extends number, A extends number[] = []> = 
| number extends N ? number
: A['length'] extends N ? A[number]
: UpTo<N, [...A, A['length']]>;

type Proper<T> =
 & {[K in keyof T | X]?: K extends X ? unknown: never}
 & T;

type Foo<Api extends any[]> = NoInfer<{ [K in UpTo<99>]?: Proper<Api[K]> }>
declare function makeApi<Api extends ZodiosEndpointDefinitions>(
    api: 
        Narrow<Api> & Foo<Api>
    ): Api;
type X = Extract<keyof ZodiosEndpointDefinition, string>
type NoInfer<T> = [T][T extends any ? 0 : never]

Somehow (Magic🌈™️ ) works with tuples

commented

Nice trick, unfortunately this will impact perf big time.
Also we have tuples in objects in tuples in the definition which make this even harder and slower if we where to use something like this :(

commented

I may try ArkType -inspired validator approach

Do you have a benchmark?


Why is makeApi api: Item[] and not api: Record<alias, Item> by the way ?