zod icon indicating copy to clipboard operation
zod copied to clipboard

Creating a custom type for number, strings, etc

Open mmahalwy opened this issue 1 year ago • 2 comments

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?

mmahalwy avatar Aug 11 '22 18:08 mmahalwy

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;

scotttrinh avatar Aug 11 '22 18:08 scotttrinh

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.

scotttrinh avatar Aug 11 '22 18:08 scotttrinh

Branded types were added in the latest release: https://github.com/colinhacks/zod/pull/1279

colinhacks avatar Sep 06 '22 04:09 colinhacks

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!

jeremyisatrecharm avatar Jan 24 '23 00:01 jeremyisatrecharm