Search code examples
typescripttrpc.io

Why does interface lose its properties when one property is defined with index signatures?


Below is the example code of my issue. I'm using tRPC and Zod in the example.

import { initTRPC, inferRouterOutputs } from '@trpc/server';
import { z } from "zod";
const t = initTRPC.create();
const { router, procedure } = t;

interface ITest {
  [key: string]: object | unknown;
  prop: string;
}

export const appRouter = router({
  foo: procedure.input(z.undefined()).query(() => {
    let test: ITest = { prop: "test" };

    return test;
  }),
});

export type AppRouter = typeof appRouter;

type bar = inferRouterOutputs<AppRouter>["foo"];
//   ^?
// type bar = { [x: string]: never; [x: number]: never; }
// expected: ITest

Here, bar is typed as

type bar = {
    [x: string]: never;
    [x: number]: never;
}

This results in a type mismatch, as the query data is no longer typed as ITest nor has the prop property, contrary to the actual data. Is this default TypeScript behavior or tRPC? How do I work around this problem?


Solution

  • The template function `` is in the file types.d.ts, and is in a section labeled as "deprecated will be removed in next major as it's v9 stuff".

    /**
     * @deprecated will be removed in next major as it's v9 stuff
     */
    ...
    export type inferSubscriptionOutput<
      TRouter extends AnyRouter,
      TPath extends string & keyof TRouter["_def"]["subscriptions"]
    > = inferObservableValue<
      inferProcedureOutput<TRouter["_def"]["subscriptions"][TPath]>
    >;
    export type inferProcedureClientError<TProcedure extends AnyProcedure> =
      inferProcedureParams<TProcedure>["_config"]["errorShape"];
    type GetInferenceHelpers<
      TType extends "input" | "output",
      TRouter extends AnyRouter
    > = {
      [TKey in keyof TRouter["_def"]["record"]]: TRouter["_def"]["record"][TKey] extends infer TRouterOrProcedure
        ? TRouterOrProcedure extends AnyRouter
          ? GetInferenceHelpers<TType, TRouterOrProcedure>
          : TRouterOrProcedure extends AnyProcedure
          ? TType extends "input"
            ? inferProcedureInput<TRouterOrProcedure>
            : inferTransformedProcedureOutput<TRouterOrProcedure>
          : never
        : never;
    };
    export type inferRouterInputs<TRouter extends AnyRouter> = GetInferenceHelpers<
      "input",
      TRouter
    >;
    export type inferRouterOutputs<TRouter extends AnyRouter> = GetInferenceHelpers<
      "output",
      TRouter
    >;
    
    

    This is not the full listing - the definition further calls other template conditional functions not shown above.

    The authors didn't test for all cases, including the case using an generic index key. That is the answer.

    As a separate comment, TypeScipt template function are qualitatively fragile and complex template functions are difficult to debug. In fact, template functions will silently return a wrong a answer if the computation gets too deep - although that may not have happened here. It could just have failed any of the extendsconditions and hit a never.

    That may be why the template function you are asking about -inferRouterOutputs - is being deprecated.


    Previous answer can be seen by viewing edit history.