Skip to content

Commit

Permalink
fix(client+server): non-records inferred as records when serializing …
Browse files Browse the repository at this point in the history
…as json (#5077)
  • Loading branch information
jussisaurio committed Nov 21, 2023
1 parent 275cf3c commit 6af030a
Show file tree
Hide file tree
Showing 2 changed files with 126 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,123 @@
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>();
});
});

describe('Zod schema serialization kitchen sink', () => {
test('Test serialization of different zod schemas against z.infer', () => {
const zObjectZArrayZRecordZTupleZUnionZIntersectionZLazyZPromiseZFunctionZMapZSetZEnumZNativeEnumZUnknownZNullableZOptionalZLiteralZBooleanZStringZNumberZBigintZDateZUndefinedZAny =
z.object({
zArray: z.array(z.string()),
zRecord: z.record(z.string()),
zTuple: z.tuple([z.string(), z.number()]),
zUnion: z.union([z.string(), z.number()]),
zIntersection: z.intersection(
z.object({ name: z.string() }),
z.object({ age: z.number() }),
),
zLazy: z.lazy(() => z.string()),
zPromise: z.promise(z.string()),
zFunction: z.function(),
zMap: z.map(z.string(), z.number()),
zSet: z.set(z.string()),
zEnum: z.enum(['foo', 'bar']),
zNativeEnum: z.nativeEnum({ foo: 1, bar: 2 }),
zUnknown: z.unknown(),
zNullable: z.nullable(z.string()),
zOptional: z.optional(z.string()),
zLiteral: z.literal('foo'),
zBoolean: z.boolean(),
zString: z.string(),
zNumber: z.number(),
zBigint: z.bigint(),
zDate: z.date(),
zUndefined: z.undefined(),
zAny: z.any(),
zArrayOptional: z.array(z.string()).optional(),
zArrayOrRecord: z.union([z.array(z.string()), z.record(z.string())]),
});

const t = initTRPC.create();

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

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

expectTypeOf<SerializedOutput>().toEqualTypeOf<{
zArray: string[];
zRecord: Record<string, string>;
zTuple: [string, number];
zUnion: string | number;
zIntersection: { name: string; age: number };
zLazy: string;
// eslint-disable-next-line @typescript-eslint/ban-types
zPromise: {};
// zFunction: (...args: any[]) => any; <-- not serialized, OK.
zMap: object;
zSet: object;
zEnum: 'foo' | 'bar';
zNativeEnum: number;
zUnknown?: unknown; // <-- why is this optional?
zNullable: string | null;
zOptional?: string | undefined;
zLiteral: 'foo';
zBoolean: boolean;
zString: string;
zNumber: number;
zBigint: never; // <-- should this be never or omitted?
zDate: string;
// zUndefined: undefined; <-- not serialized, OK.
zAny?: any; // <-- why is this optional?
zArrayOptional?: string[] | undefined;
zArrayOrRecord: string[] | Record<string, string>;
}>();
});
});

3 comments on commit 6af030a

@vercel
Copy link

@vercel vercel bot commented on 6af030a Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

www – ./www

www-git-main-trpc.vercel.app
trpc.io
www-trpc.vercel.app
alpha.trpc.io
www.trpc.io
beta.trpc.io

@vercel
Copy link

@vercel vercel bot commented on 6af030a Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-prisma-starter – ./examples/next-prisma-starter

next-prisma-starter-trpc.vercel.app
nextjs.trpc.io
next-prisma-starter-git-main-trpc.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 6af030a Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

og-image – ./www/og-image

og-image-git-main-trpc.vercel.app
og-image-trpc.vercel.app
og-image-three-neon.vercel.app
og-image.trpc.io

Please sign in to comment.