bug: output type incorrectly inferred when compiling typescript for client when using zod .catchall()
Provide environment information
System:
OS: macOS 12.6
CPU: (10) arm64 Apple M1 Pro
Memory: 97.91 MB / 16.00 GB
Shell: 5.8.1 - /bin/zsh
Binaries:
Node: 16.13.1 - ~/.nvm/versions/node/v16.13.1/bin/node
Yarn: 1.22.19 - ~/.nvm/versions/node/v16.13.1/bin/yarn
npm: 8.1.2 - ~/.nvm/versions/node/v16.13.1/bin/npm
Watchman: 2022.10.03.00 - /opt/homebrew/bin/watchman
Browsers:
Brave Browser: 107.1.45.127
Safari: 16.0
npmPackages:
@trpc/client: ^10.4.2 => 10.4.2
@trpc/server: ^10.4.2 => 10.4.2
typescript: ^4.9.3 => 4.9.3
Describe the bug
When using an zod object with .catchall() for the output of a procedure, it will incorrectly infer the type of the object when compiling the code using typescript for usage in another library.
I've created a minimal example to reproduce the issue. Let's say we have a server with one procedure test. This procedure takes as both input and output a z object with a required property name and a catchall() of z.unknown. this should generate the following interface:
{
[x: string]: unknown;
name: string;
}
In most cases it does generate that interface, except for the output model of the trpc client.
If we have the following code.
import { inferRouterInputs, inferRouterOutputs, initTRPC } from "@trpc/server";
import { createTRPCProxyClient } from "@trpc/client";
import { z } from "zod";
const t = initTRPC.create({});
// Simple object with name as string and allows any other properties as unknown
const zObject = z
.object({
name: z.string(),
})
.catchall(z.unknown());
const router = t.router({
// Test takes the zObject as both input and output
test: t.procedure
.input(zObject)
.output(zObject)
.mutation(({ input }) => {
return input;
}),
});
// Client is based on the type of the router
type AppRouter = typeof router;
// correctly specifies the input type
export const input = {} as inferRouterInputs<AppRouter>["test"];
// correctly specifies the output type
export const output = {} as inferRouterOutputs<AppRouter>["test"];
// correctly specifies the input type, incorrectly specifies the output type
export const client = createTRPCProxyClient<AppRouter>({
links: [],
});
It will generate the following .d.ts bundle:
export declare const input: {
[x: string]: unknown;
name: string;
};
export declare const output: {
[x: string]: unknown;
name: string;
};
export declare const client: {
test: {
mutate: (input: {
[x: string]: unknown;
name: string;
}, opts?: import("@trpc/server").ProcedureOptions | undefined) => Promise<{
[x: string]: never;
[x: number]: never;
}>;
};
};
As you can see the input and output variables both have the correct type inferred. The client.test.mutate input also has the correct type inferred, however if you look at the return type of the client.test.mutate it is :
{
[x: string]: never;
[x: number]: never;
}
If I were to remove the .catchall from the zod object (for e.g. .passthrough) it will work, but I lose the ability to add extra properties in TS.
This could be a zod issue (if so please let me know and I'll open this issue there instead). But as the type inference works in all cases except in the return type of the trpc client I thought it may be something to do with trpc instead.
Link to reproduction
https://github.com/TimoGlastra/trpc-zod-issue
To reproduce
Clone the repo above, run yarn install then run yarn tsc. This will build the d.ts file in build/index.d.ts and you can see the output as described above.
Additional information
Would be willing to contribute a PR but wouldn't know where to start or whether this is solveable.
๐จโ๐งโ๐ฆ Contributing
- [X] ๐โโ๏ธ Yes, I'd be down to file a PR fixing this bug!
This seems like @jgoux territory - WDYT could cause this? I don't export my tRPC backend "as an SDK" myself
This seems like @jgoux territory - WDYT could cause this? I don't export my tRPC backend "as an SDK" myself
I'm off to a Snaplet gathering event, but I'll take a look when I'm back next week! ๐ค
I just updated the reproduction to the latest version of trpc and zod, and interestingly enough it now also encodes the output variable as:
{
[x: string]: never;
[x: number]: never;
}
It seems the version of zod does not influence it (previous was 3.19.1, new is 3.20.2, both give same output).
Any ideas why this has changed between 10.4.2 and 10.7.0?
You can see the changes in what is generated between the versions here: https://github.com/TimoGlastra/trpc-zod-issue/commit/a55104cbef67cade7753f031e4d7869f0159f879
Hi there, I have been meaning to get into OSS contribution for a while, and thought I'd start out with this issue, since I am loving tRPC so far and saw this issue was open for a while.
I boiled it down to being an issue in the Serialize<> utility type, which I believe was taken from the remix repository. I have updated the utility type to reflect their changes & modified it a bit such that it is able to deal with the unknown type. I've also added a test for this.
Since Serialize<> did not have any previous tests with expect-type I am not sure if the changes break anything else. remix unfortunately also doesn't have tests for the utility type that I could take over.
Just let me know if there is anything might have overlooked.
Seems to work in the latest version of tRPC AFAICT
output
export declare const input: {
[x: string]: unknown;
name: string;
};
export declare const output: {
[x: string]: never;
[x: number]: never;
};
export declare const client: {
test: {
mutate: import("@trpc/client").Resolver<import("@trpc/server").BuildProcedure<"mutation", {
_config: import("@trpc/server").RootConfig<{
ctx: object;
meta: object;
errorShape: import("@trpc/server").DefaultErrorShape;
transformer: import("@trpc/server").DefaultDataTransformer;
}>;
_meta: object;
_ctx_out: object;
_input_in: {
[x: string]: unknown;
name: string;
};
_input_out: {
[x: string]: unknown;
name: string;
};
_output_in: {
[x: string]: unknown;
name: string;
};
_output_out: {
[x: string]: unknown;
name: string;
};
}, unknown>>;
};
};
If you add export { z } from 'zod' you get:
export { z } from 'zod';
export declare const input: {
[x: string]: unknown;
name: string;
};
export declare const output: {
[x: string]: never;
[x: number]: never;
};
export declare const client: {
test: {
mutate: import("@trpc/client").Resolver<import("@trpc/server").BuildProcedure<"mutation", {
_config: import("@trpc/server").RootConfig<{
ctx: object;
meta: object;
errorShape: import("@trpc/server").DefaultErrorShape;
transformer: import("@trpc/server").DefaultDataTransformer;
}>;
_meta: object;
_ctx_out: object;
_input_in: {
[x: string]: unknown;
name: string;
};
_input_out: {
[x: string]: unknown;
name: string;
};
_output_in: {
[x: string]: unknown;
name: string;
};
_output_out: {
[x: string]: unknown;
name: string;
};
}, unknown>>;
};
};