zod
zod copied to clipboard
Creating a custom type for number, strings, etc
We use numbers all over our codebase to represent numbers, dollar value, percentages, etc. Ideally, we'd like to keep the zod type along with a typescript type that represents that type.
Here's an example:
type DollarValue = number;
const dollarValueSchema = z.number();
Ideally, when using z.infer<typeof dollarValueSchema>
we get DollarValue
type, instead of number
.
Thoughts?
This is similar to the idea behind "branded" types, which is discussed here: https://github.com/colinhacks/zod/issues/3#issuecomment-794459823
TL;DR you can get that with roughly:
type Tagged<T, Tag> = T & { __tag: Tag };
type DollarValue = Tagged<string, 'DollarValue'>;
const dollar: z.Schema<DollarValue> = z.number() as any;
The major difficulty here is the TypeScript uses structural type rather than nominal typing so if something is structurally compatible, they are equivalent and the compiler doesn't treat them differently. This branding trick lies to the compiler and tells it that it has a special __tag
property (when it doesn't really) but that forces the compiler to treat these number
values differently than other numbers.
Branded types were added in the latest release: https://github.com/colinhacks/zod/pull/1279
Is https://github.com/colinhacks/zod/issues/3#issuecomment-1173460892 still the recommended way to brand strings-only or is brand ok?
I am using .brand
for a string. For example - const userIdSchema = z.string().brand<"userId">()
but am finding these brands get ignored when used with other schemas and infer.
So I am using this as part of my API and later re-using that API type, only to get an error like:
Type 'Partial<Record<"9" | "4" | "1", Partial<Record<(string & BRAND<"EditedUserId">) | (string & BRAND<"NoneditedUserId">), string>>>>' is not assignable to type 'Partial<Record<"9" | "4" | "1", Record<string, string>>>'
I see the .brand
documentation doesn't have branded strings (but rather has z.object
), is that because they are not supported?
edit: I was using z.input
and not z.infer
which is incorrect as is excellently described here. Brands are very helpful - thank you for adding them!