Typescript API contract, type instantiation is excessively deep and possibly infinite

165 views Asked by At

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'
>
0

There are 0 answers