Stronger typing on `makeApi`
andenacitelli opened this issue · comments
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.
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:
in zodios v11 you'll be able to use as const and satisfies together as a workaround:
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.
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
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
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
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
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 :(
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 ?