bug: Invalid serialization of zod branded tuples
Provide environment information
System:
OS: macOS 15.3.1
CPU: (16) arm64 Apple M3 Max
Memory: 17.81 GB / 48.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 18.16.0 - ~/.nvm/versions/node/v18.16.0/bin/node
Yarn: 1.22.21 - ~/.nvm/versions/node/v18.16.0/bin/yarn
npm: 9.5.1 - ~/.nvm/versions/node/v18.16.0/bin/npm
pnpm: 8.14.3 - ~/.nvm/versions/node/v18.16.0/bin/pnpm
Browsers:
Chrome: 133.0.6943.55
Safari: 18.3
npmPackages:
@trpc/client: 11.0.0-rc.741 => 11.0.0-rc.741+caed91ee7
@trpc/server: 11.0.0-rc.741 => 11.0.0-rc.741+caed91ee7
typescript: 5.7.3 => 5.7.3 Describe the bug
The trpc client fails to infer the output type of a zod-serialized tuple.
For a procedure like this
export const Point = z
.tuple([z.number(), z.number(), z.number()])
.brand("Point");
export type Point = z.infer<typeof Point>;
export const appRouter = t.router({
getPoint: t.procedure.query(async () => {
return {
point: [1, 2, 3] as Point,
};
}),
});const result = await trpc.getPoint.query(); // has type{
point: SerializeTuple<[number, number, number] & BRAND<"Point">>;
}This type isn't assignable to Point:
Type 'SerializeTuple<[number, number, number] & BRAND<"Point">>' is not assignable to type '[number, number, number] & BRAND<"Point">'.
Type 'SerializeTuple<[number, number, number] & BRAND<"Point">>' is not assignable to type '[number, number, number]'.
Types of property 'length' are incompatible.
Type 'number' is not assignable to type '3'Workaround
I can define a dummy transformer to make trpc bail out and not try to transform the types. Then I get the right type in the frontend.
Link to reproduction
https://github.com/davazp/trpc-bug-branded-tuples
To reproduce
https://github.com/davazp/trpc-bug-branded-tuples
Additional information
The SerializedTuple definition here
/** JSON serialize [tuples](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) */
type SerializeTuple<T extends [unknown, ...unknown[]]> = {
[K in keyof T]: T[K] extends NonJsonPrimitive ? null : Serialize<T[K]>;
};The indexing here will treat this as an object
type T = Serialize<[1,2,3] & {__brand: void}>;type T = {
[x: number]: 1 | 3 | 2;
0: 1;
1: 2;
2: 3;
length: 1 | 3 | 2;
toString: null;
toLocaleString: null;
pop: null;
push: null;
concat: null;
join: null;
reverse: null;
shift: null;
slice: null;
sort: null;
splice: null;
... 23 more ...;
__brand: 1 | ... 1 more ... | 2;
}Some typewizardy is needed. However, I guess this affects other type of branded types and other situations.
It would be simpler if, instead, trpc would just check that the output type is JSON compatible, giving an error otherwise, but returning the original type unmodified.
But sure, then procedures could not return Date for instance. I think it is fair, define a serializer if you want that? But it's a breaking change.
Contributing
- ♂️ Yes, I'd be down to file a PR fixing this bug!
Source: trpc/trpc