Skip to content

Commit 6c9b8b5

Browse files
gary-ixNicolapps
andauthored
add: preserve branding for object types (#868)
Co-authored-by: Nicolas Ettlin <nicolas@convex.dev>
1 parent 447bad0 commit 6c9b8b5

File tree

6 files changed

+210
-6
lines changed

6 files changed

+210
-6
lines changed

packages/convex-helpers/server/zod3.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,69 @@ expectTypeOf<z.output<typeof _n>>().toEqualTypeOf<number & z.BRAND<"brand">>();
935935
expectTypeOf<z.input<typeof _i>>().toEqualTypeOf<bigint & z.BRAND<"brand">>();
936936
expectTypeOf<z.output<typeof _i>>().toEqualTypeOf<bigint & z.BRAND<"brand">>();
937937

938+
// Test branded objects preserve their brand
939+
const ZBrandedObject = z
940+
.object({ name: z.string(), age: z.number() })
941+
.brand<"User">();
942+
const ZBrandedObject2 = z.object({ id: z.string() }).brand("UserId");
943+
const _vBrandedObject = zodToConvex(ZBrandedObject);
944+
const _vBrandedObject2 = zodToConvex(ZBrandedObject2);
945+
const _vBrandedObjectOutput = zodOutputToConvex(ZBrandedObject);
946+
const _vBrandedObjectOutput2 = zodOutputToConvex(ZBrandedObject2);
947+
948+
// Verify branded objects have the brand in their type
949+
type _BrandedObjectType = Infer<typeof _vBrandedObject>;
950+
type _BrandedObject2Type = Infer<typeof _vBrandedObject2>;
951+
type _BrandedObjectOutputType = Infer<typeof _vBrandedObjectOutput>;
952+
type _BrandedObjectOutput2Type = Infer<typeof _vBrandedObjectOutput2>;
953+
954+
expectTypeOf<_BrandedObjectType>().toEqualTypeOf<
955+
{ name: string; age: number } & z.BRAND<"User">
956+
>();
957+
expectTypeOf<_BrandedObject2Type>().toEqualTypeOf<
958+
{ id: string } & z.BRAND<"UserId">
959+
>();
960+
expectTypeOf<_BrandedObjectOutputType>().toEqualTypeOf<
961+
{ name: string; age: number } & z.BRAND<"User">
962+
>();
963+
expectTypeOf<_BrandedObjectOutput2Type>().toEqualTypeOf<
964+
{ id: string } & z.BRAND<"UserId">
965+
>();
966+
967+
// Test more complex branded object (like the user's example)
968+
const ZUserBase = z.object({
969+
email: z.string().email(),
970+
name: z.string(),
971+
createdAt: z.number(),
972+
});
973+
const ZUser = ZUserBase.extend({
974+
role: z.enum(["admin", "user"]),
975+
}).brand<"User">();
976+
const ZUser2 = ZUserBase.extend({ role: z.enum(["admin", "user"]) }).brand(
977+
"User2",
978+
);
979+
const _vUser = zodToConvex(ZUser);
980+
const _vUser2 = zodToConvex(ZUser2);
981+
982+
type UserType = Infer<typeof _vUser>;
983+
type User2Type = Infer<typeof _vUser2>;
984+
expectTypeOf<UserType>().toEqualTypeOf<
985+
{
986+
email: string;
987+
name: string;
988+
createdAt: number;
989+
role: "admin" | "user";
990+
} & z.BRAND<"User">
991+
>();
992+
expectTypeOf<User2Type>().toEqualTypeOf<
993+
{
994+
email: string;
995+
name: string;
996+
createdAt: number;
997+
role: "admin" | "user";
998+
} & z.BRAND<"User2">
999+
>();
1000+
9381001
function sameType<T, U>(_t: T, _u: U): Equals<T, U> {
9391002
return true as any;
9401003
}

packages/convex-helpers/server/zod3.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,25 @@ export type ConvexValidatorFromZod<Z extends z.ZodTypeAny> =
764764
? VInt64<
765765
bigint & z.BRAND<Brand>
766766
>
767-
: ConvexValidatorFromZod<Inner>
767+
: Inner extends z.ZodObject<
768+
infer ZodShape
769+
>
770+
? VObject<
771+
ObjectType<{
772+
[key in keyof ZodShape]: ZodShape[key] extends z.ZodTypeAny
773+
? ConvexValidatorFromZod<
774+
ZodShape[key]
775+
>
776+
: never;
777+
}> &
778+
z.BRAND<Brand>,
779+
{
780+
[key in keyof ZodShape]: ConvexValidatorFromZod<
781+
ZodShape[key]
782+
>;
783+
}
784+
>
785+
: ConvexValidatorFromZod<Inner>
768786
: Z extends z.ZodDefault<
769787
infer Inner
770788
> // Treat like optional
@@ -1170,7 +1188,25 @@ export type ConvexValidatorFromZodOutput<Z extends z.ZodTypeAny> =
11701188
? VInt64<
11711189
bigint & z.BRAND<Brand>
11721190
>
1173-
: ConvexValidatorFromZodOutput<Inner>
1191+
: Inner extends z.ZodObject<
1192+
infer ZodShape
1193+
>
1194+
? VObject<
1195+
ObjectType<{
1196+
[key in keyof ZodShape]: ZodShape[key] extends z.ZodTypeAny
1197+
? ConvexValidatorFromZodOutput<
1198+
ZodShape[key]
1199+
>
1200+
: never;
1201+
}> &
1202+
z.BRAND<Brand>,
1203+
{
1204+
[key in keyof ZodShape]: ConvexValidatorFromZodOutput<
1205+
ZodShape[key]
1206+
>;
1207+
}
1208+
>
1209+
: ConvexValidatorFromZodOutput<Inner>
11741210
: Z extends z.ZodRecord<
11751211
infer K,
11761212
infer V

packages/convex-helpers/server/zod4.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,10 +1244,20 @@ type ConvexValidatorFromZodCommon<
12441244
zCore.$brand<Brand>,
12451245
IsOptional
12461246
>
1247-
: ConvexValidatorFromZod<
1248-
Inner,
1249-
IsOptional
1250-
>
1247+
: Inner extends zCore.$ZodObject<
1248+
infer Fields extends
1249+
Readonly<zCore.$ZodShape>
1250+
>
1251+
? VObject<
1252+
zCore.infer<Inner> &
1253+
zCore.$brand<Brand>,
1254+
ConvexObjectFromZodShape<Fields>,
1255+
IsOptional
1256+
>
1257+
: ConvexValidatorFromZod<
1258+
Inner,
1259+
IsOptional
1260+
>
12511261
: // z.record()
12521262
Z extends zCore.$ZodRecord<
12531263
infer Key extends

packages/convex-helpers/server/zod4.zod3.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,69 @@ expectTypeOf<z.output<typeof _n>>().toEqualTypeOf<number & z.BRAND<"brand">>();
841841
expectTypeOf<z.input<typeof _i>>().toEqualTypeOf<bigint>();
842842
expectTypeOf<z.output<typeof _i>>().toEqualTypeOf<bigint & z.BRAND<"brand">>();
843843

844+
// Test branded objects preserve their brand
845+
const ZBrandedObject = z
846+
.object({ name: z.string(), age: z.number() })
847+
.brand<"User">();
848+
const ZBrandedObject2 = z.object({ id: z.string() }).brand("UserId");
849+
const _vBrandedObject = zodToConvex(ZBrandedObject);
850+
const _vBrandedObject2 = zodToConvex(ZBrandedObject2);
851+
const _vBrandedObjectOutput = zodOutputToConvex(ZBrandedObject);
852+
const _vBrandedObjectOutput2 = zodOutputToConvex(ZBrandedObject2);
853+
854+
// Verify branded objects have the brand in their type
855+
type _BrandedObjectType = Infer<typeof _vBrandedObject>;
856+
type _BrandedObject2Type = Infer<typeof _vBrandedObject2>;
857+
type _BrandedObjectOutputType = Infer<typeof _vBrandedObjectOutput>;
858+
type _BrandedObjectOutput2Type = Infer<typeof _vBrandedObjectOutput2>;
859+
860+
expectTypeOf<_BrandedObjectType>().toEqualTypeOf<
861+
{ name: string; age: number } & z.BRAND<"User">
862+
>();
863+
expectTypeOf<_BrandedObject2Type>().toEqualTypeOf<
864+
{ id: string } & z.BRAND<"UserId">
865+
>();
866+
expectTypeOf<_BrandedObjectOutputType>().toEqualTypeOf<
867+
{ name: string; age: number } & z.BRAND<"User">
868+
>();
869+
expectTypeOf<_BrandedObjectOutput2Type>().toEqualTypeOf<
870+
{ id: string } & z.BRAND<"UserId">
871+
>();
872+
873+
// Test more complex branded object (like the user's example)
874+
const ZUserBase = z.object({
875+
email: z.email(),
876+
name: z.string(),
877+
createdAt: z.number(),
878+
});
879+
const ZUser = ZUserBase.extend({
880+
role: z.enum(["admin", "user"]),
881+
}).brand<"User">();
882+
const ZUser2 = ZUserBase.extend({ role: z.enum(["admin", "user"]) }).brand(
883+
"User2",
884+
);
885+
const _vUser = zodToConvex(ZUser);
886+
const _vUser2 = zodToConvex(ZUser2);
887+
888+
type UserType = Infer<typeof _vUser>;
889+
type User2Type = Infer<typeof _vUser2>;
890+
expectTypeOf<UserType>().toEqualTypeOf<
891+
{
892+
email: string;
893+
name: string;
894+
createdAt: number;
895+
role: "admin" | "user";
896+
} & z.BRAND<"User">
897+
>();
898+
expectTypeOf<User2Type>().toEqualTypeOf<
899+
{
900+
email: string;
901+
name: string;
902+
createdAt: number;
903+
role: "admin" | "user";
904+
} & z.BRAND<"User2">
905+
>();
906+
844907
function sameType<T, U>(_t: T, _u: U): Equals<T, U> {
845908
return true as any;
846909
}

packages/convex-helpers/server/zod4.zodtoconvex.mini.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
VFloat64,
99
VLiteral,
1010
VNull,
11+
VObject,
1112
VOptional,
1213
VString,
1314
VUnion,
@@ -141,6 +142,21 @@ describe("zodToConvex + zodOutputToConvex", () => {
141142
v.number() as VFloat64<number & zCore.$brand<"myBrand">>,
142143
);
143144
});
145+
test("object", () => {
146+
testZodToConvexInputAndOutput(
147+
z.object({ name: z.string() }).brand("myBrand"),
148+
v.object({ name: v.string() }) as VObject<
149+
{
150+
name: string;
151+
} & zCore.$brand<"myBrand">,
152+
{
153+
name: VString<string, "required">;
154+
},
155+
"required",
156+
"name"
157+
>,
158+
);
159+
});
144160
});
145161

146162
test("object", () => {

packages/convex-helpers/server/zod4.zodtoconvex.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
VFloat64,
1111
VLiteral,
1212
VNull,
13+
VObject,
1314
VOptional,
1415
VString,
1516
VUnion,
@@ -142,6 +143,21 @@ describe("zodToConvex + zodOutputToConvex", () => {
142143
v.number() as VFloat64<number & zCore.$brand<"myBrand">>,
143144
);
144145
});
146+
test("object", () => {
147+
testZodToConvexInputAndOutput(
148+
z.object({ name: z.string() }).brand("myBrand"),
149+
v.object({ name: v.string() }) as VObject<
150+
{
151+
name: string;
152+
} & zCore.$brand<"myBrand">,
153+
{
154+
name: VString<string, "required">;
155+
},
156+
"required",
157+
"name"
158+
>,
159+
);
160+
});
145161
});
146162

147163
test("object", () => {

0 commit comments

Comments
 (0)