3

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?

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
SimHoZebs
  • 89
  • 1
  • 7

2 Answers2

0

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.

Craig Hicks
  • 2,199
  • 20
  • 35
  • I received multiple answers (which they deleted for some reason) with conflicting reasons. Some people think it's default Typescript behavior and the type needs to be adjusted; some believe it's a bug in tRPC and that the type should be handled correctly. Could you explain how the behavior of `object|unknown` relates with index signatures? Why would tRPC convert the value to `never` (assuming tRPC isn't the problem here)? – SimHoZebs Jul 11 '23 at 00:15
  • Did changing from `object | unknown` to `unknow` or `any` make a difference? – Craig Hicks Jul 11 '23 at 04:25
  • Updated answer after reviewing source. – Craig Hicks Jul 11 '23 at 06:49
  • 1
    Changing `unknown` to `any` does solve the issue. However, that's not an option as the community around the library with the problematic types [decided it was better for typesafety](https://github.com/plaid/plaid-node/issues/491). The actual situation is with tRPC client query functions returning the wrong types, not exactly with `inferRouterOutputs`; I used it because their result was identical, but now I see that I might have misled you by doing so. My bad. Still, your answer suggests that it is most probably a bug in tRPC and not TypeScript. Thanks. – SimHoZebs Jul 11 '23 at 17:22
  • @SimHoZebs - Thanks for delivering on this slippery problem. Not wanting to change the answer that was already selected, I added another answer with additional notes. – Craig Hicks Jul 13 '23 at 23:40
0

Some additional information that is highly relevant to posted question, but might or might not be related to the observed behavior:

Note1:

type X = object | unknown; // tooltip shows "type X = unknown"
type Y = object | any; // tooltip shows "type Y = any"

TypeScriptPlayground

object|unknown is equivalent to unknown (see TypeScript Documentation "New Unknown Top Type"). That means writing object|unknown should be no different than writing unknown. However, there might be side effects in TypeScript template functions where is does make a difference.

Likewise object|any is equivalent to any.

Note2 Although it is probably not related to your issue - Use of object instead of {[x:string]:unknown} or {[x:string]:any} may have unexpected side effects, as shown in the following code:

function foo(obj: object) {
    for (const key in obj) {
        const val = obj[key]; // Compilation error: type 'string' can't be used to index type '{}'
    }
}

function bar(obj: { [key: string]: unknown }) {
    for (const key in obj) {
        const val = obj[key]; // works :)
    }
}

See this stackoverflow answer.

Note3

I modified the code in this working online trpc site to copy in your in addition, changing a any variable names that collided.

I found that the result of using

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

was

"bar = {}"

The type {} in Typescript is actually not an object with no properties - it is instead a special case treated as { [x:string]:any }.

So at least in that test in that trpc online environment, the prop:string property was effectively converted to prop: any.

I mention this because you said in your comment that

interface ITest {
  [key: string]: any;
  prop: string;
}

worked for you.

Craig Hicks
  • 2,199
  • 20
  • 35