Skip to content

Commit

Permalink
fix(client+server): non-records inferred as records
Browse files Browse the repository at this point in the history
  • Loading branch information
jussisaurio committed Nov 21, 2023
1 parent 275cf3c commit 2bbd6af
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 1 deletion.
4 changes: 3 additions & 1 deletion packages/server/src/shared/internal/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type IsAny<T> = 0 extends T & 1 ? true : false;
// support it as both a Primitive and a NonJsonPrimitive
type JsonReturnable = JsonPrimitive | undefined;

type IsRecord<T> = keyof WithoutIndexSignature<T> extends never ? true : false;

/* prettier-ignore */
export type Serialize<T> =
IsAny<T> extends true ? any :
Expand All @@ -29,7 +31,7 @@ export type Serialize<T> =
T extends [] ? [] :
T extends [unknown, ...unknown[]] ? SerializeTuple<T> :
T extends readonly (infer U)[] ? (U extends NonJsonPrimitive ? null : Serialize<U>)[] :
Record<never, never> extends T ? Record<keyof T, Serialize<T[keyof T]>> :
IsRecord<T> extends true ? Record<keyof T, Serialize<T[keyof T]>> :
T extends object ? Simplify<SerializeObject<UndefinedToOptional<T>>> :
never;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { inferRouterOutputs, initTRPC } from '@trpc/server';
import * as z from 'zod';

describe('Non-records should not erroneously be inferred as Records in serialized types', () => {
const zChange = z.object({
status: z
.tuple([
z.literal('tmp').or(z.literal('active')),
z.literal('active').or(z.literal('disabled')),
])
.optional(),
validFrom: z.tuple([z.string().nullable(), z.string()]).optional(),
validTo: z.tuple([z.string().nullable(), z.string().nullable()]).optional(),
canceled: z.tuple([z.boolean(), z.boolean()]).optional(),
startsAt: z
.tuple([z.string().nullable(), z.string().nullable()])
.optional(),
endsAt: z.tuple([z.string().nullable(), z.string().nullable()]).optional(),
});
type Change = z.infer<typeof zChange>;

test('should be inferred as object', () => {
const t = initTRPC.create();

const router = t.router({
createProject: t.procedure.output(zChange).query(() => {
return zChange.parse(null as any);
}),
createProjectNoExplicitOutput: t.procedure.query(() => {
return zChange.parse(null as any);
}),
});

type SerializedOutput = inferRouterOutputs<typeof router>['createProject'];
type SerializedOutputNoExplicitOutput = inferRouterOutputs<
typeof router
>['createProjectNoExplicitOutput'];

expectTypeOf<SerializedOutput>().toEqualTypeOf<Change>();
expectTypeOf<SerializedOutputNoExplicitOutput>().toEqualTypeOf<Change>();
});
});

0 comments on commit 2bbd6af

Please sign in to comment.