I am trying to come up with a simple API contract similar to ts-rest, but strip away a lot of things I don't need. Also I want to use json schema instead of Zod.
Most of the code is copied from ts-rest and modified to my needs. Everything works as expected on runtime except that I get a possible infinite error from the typescript compiler.
And no matter what I try, I can't fix it. Tried also typescript 5.3 - no luck.
The compiler warning is in this section in the recursivelyRegisterRoutes
function:
if ("contract" in serverImpl && "routes" in serverImpl) {
recursivelyRegisterRoutes(
serverImpl.routes,
serverImpl.contract,
fastify
);
}
import type {
FastifyInstance,
FastifyPluginAsync,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
RouteOptions
} from "fastify";
import type { FromSchema, JSONSchema } from "json-schema-to-ts";
type AppRouteCommon = {
path: string
params?: JSONSchema
query?: JSONSchema
response?: Record<number, JSONSchema>
}
export type AppRouteQuery = AppRouteCommon & {
method: 'GET'
}
export type AppRouteMutation = AppRouteCommon & {
method: 'POST' | 'DELETE' | 'PUT' | 'PATCH'
body?: JSONSchema
}
export type AppRoute = AppRouteQuery | AppRouteMutation
export type AppRouter = {
[key: string]: AppRouter | AppRoute
}
type AppRouteImplementation<T extends AppRoute> = Omit<
RouteOptions<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
{
Params: T["params"] extends JSONSchema ? FromSchema<T["params"]> : never;
Querystring: T["query"] extends JSONSchema
? FromSchema<T["query"]>
: never;
Body: T extends AppRouteMutation
? T["body"] extends JSONSchema
? FromSchema<T["body"]>
: never
: never;
}
>,
"method" | "url"
>;
type RecursiveServerObj<T extends AppRouter> = {
[TKey in keyof T]: T[TKey] extends AppRouter
? InitialisedServer<T[TKey]> | RecursiveServerObj<T[TKey]>
: T[TKey] extends AppRoute
? AppRouteImplementation<T[TKey]>
: never;
};
type InitialisedServer<TContract extends AppRouter> = {
contract: TContract;
routes: RecursiveServerObj<TContract>;
};
export const initServer = <T extends AppRouter>(
contract: T,
routes: RecursiveServerObj<T>
): InitialisedServer<T> => ({
contract,
routes
});
const recursivelyRegisterRoutes = <T extends AppRouter>(
serverImpl: RecursiveServerObj<T>,
contract: T,
fastify: FastifyInstance
) => {
if (typeof serverImpl === "object") {
if ("contract" in serverImpl && "routes" in serverImpl) {
recursivelyRegisterRoutes(
serverImpl.routes,
serverImpl.contract,
fastify
);
} else {
if (typeof serverImpl["handler"] !== "function") {
for (const key in serverImpl) {
recursivelyRegisterRoutes(
(serverImpl[key] as unknown) as RecursiveServerObj<T>,
(contract[key] as unknown) as T,
fastify
);
}
} else {
const route = (contract as unknown) as AppRoute;
fastify.route({
method: route.method,
url: route.path,
...((serverImpl as unknown) as AppRouteImplementation<AppRoute>)
});
}
}
}
};
export const initPlugin = <T extends AppRouter>(
server: InitialisedServer<T>
): FastifyPluginAsync => async (fastify) => {
recursivelyRegisterRoutes(server.routes, server.contract, fastify);
};
Here is a working example: codesandbox, TypeScript playground
UPDATE
Just solved it by changing the AppRouteImplementation:
type AppRouteImplementation<T extends AppRoute> = Omit<
RouteOptions<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
{
Params: T['params'] extends infer R ? FromSchema<R extends JSONSchema ? R : never> : never
Querystring: T['query'] extends infer R ? FromSchema<R extends JSONSchema ? R : never> : never
Body: T extends AppRouteMutation
? T['body'] extends infer R
? FromSchema<R extends JSONSchema ? R : never>
: never
: never
}
>,
'method' | 'url'
>