# zod筆記 ## zod schema vs interface ### interface缺陷 只能做型別檢測,如遇到遇到data是空的ts並不會幫你檢測出來,但zod可以 ```jsonld= //../data/data.json { "results":[ { "id":0, "name":"danny", "job":"develop" } ] } ``` ```jsonld= //../data/data2.json { } ``` ```javascript= import fs from "fs"; import { z, ZodSchema } from "zod"; const ResultSchema = z.object({ results: z.array( z.object({ id: z.number(), name: z.string(), job: z.string(), }) ) } ) type ResultZod = z.infer<typeof ResultSchema> interface Result { results: { id: number name: string job: string }[] } //ResultZod 和 Result型別一樣 function PrintResultWithOutZod(result: Result) { result.results.forEach((result) => { console.log(result.job) }) } function PrintResultWithZod(result: ResultZod) { if (ResultSchema.safeParse(result).success) { console.log('Good data') result.results.forEach((result) => { console.log(result.job) }) } else { console.log('Bad data') } } const result: Result = JSON.parse(fs.readFileSync('../data/data2.json', "utf-8")) console.log(result) PrintResultWithZod(result) const InfoSchema = z.object({ name: z.string(), age: z.number().min(20) }) function CustomerSchemaValidate<T>(schema: z.Schema<T>, value: T) { const data = schema.safeParse(value) if (data.success) { console.log('success') } else { console.log('error') } } CustomerSchemaValidate(InfoSchema, { name: 'danny', age: 20 }) ``` ## 比較error! ### 用 interface ![](https://i.imgur.com/9wFchtJ.png) ### 用zod ![](https://i.imgur.com/xCjGst8.png) **是不是代碼很簡潔呢** ## 表單驗證與zod zod的特性很特別的在於可以自定義error message,利用superRefine去判斷schema欄位中是否有等值,如果錯誤可以自定義error message ```typescript= import { z, ZodError, ZodIssueCode } from "zod"; const LoginFormSchema = z.object({ email: z.string().email(), password: z.string().min(4), confirmPassword: z.string().min(4), remember: z.boolean().default(false).optional(), }).superRefine(({ confirmPassword, password }, ctx) => { if (password !== confirmPassword) { ctx.addIssue({ code: 'custom', message: 'The passwords did not match', path: ["confirmPassword"] }) } }) const LoginFormSchema2 = z.object({ email: z.string().email(), password: z.string().min(4), confirmPassword: z.string().min(4), remember: z.boolean().default(false).optional(), }).refine( ({ confirmPassword, password }) => { return confirmPassword === password }, { path: ["confirmPassword"], message: "Passwords don't match", } ) const useForm = <TValues>(schema: z.Schema<TValues>, onSubmit: (values: TValues) => void) => { return { onSubmit: (value: unknown) => { try { const newValue = schema.parse(value) onSubmit(newValue) } catch (e) { if (e instanceof ZodError) { const { message, path } = e.errors[0] console.log(path + ': ' + message) } } } } } const form = useForm(LoginFormSchema, (values) => { console.log(values) }) const password = { email: 'hiunji64@gmail.com', password: '1111', confirmPassword: '1111', } form.onSubmit(password) // form.checkoutError(password) ``` ## 用schema取特定data ```typescript= import { z } from "zod"; const commentSchema = z.object({ email: z.string() }) const commentSchemaResult = z.array( commentSchema ) const genericFetch = <ZSchema extends z.ZodSchema>( url: string, schema: ZSchema ): Promise<z.infer<ZSchema>> => { return fetch(url) .then(res => res.json()) .then(result => schema.parse(result)) } const getComment = async () => { const aaa = await genericFetch('https://jsonplaceholder.typicode.com/comments?_page=2', commentSchemaResult) // aaa.forEach(item=>{ // console.log(item.email) // }) } getComment() ``` ```typescript //origin data { "postId": 3, "id": 11, "name": "fugit labore quia mollitia quas deserunt nostrum sunt", "email": "Veronica_Goodwin@timmothy.net", "body": "ut dolorum nostrum id quia aut est\nfuga est inventore vel eligendi explicabo quis consectetur\naut occaecati repellat id natus quo est\nut blanditiis quia ut vel ut maiores ea" } //need data { "email": "Veronica_Goodwin@timmothy.net", } ``` ## 甚至你還可以自訂欄位 ```typescript= const StarWarsPerson = z.object({ name: z.string(), }).transform((person) => ({ ...person, nameAsArray: person.name.split(" ") })) const StarWarsPeopleResults = z.object({ results: z.array(StarWarsPerson) }) const fetchStarWarsPeople = async () => { const data = await fetch( "https://www.totaltypescript.com/swapi/people.json", ).then((res) => res.json()); const parseData = StarWarsPeopleResults.parse(data) console.log(parseData) // parseData.results.forEach(item=>{ // console.log(item.name) // }) return parseData } fetchStarWarsPeople() ``` ## JSON type ```typescript const jsonSchema = z.string().transform(async (string, ctx) => { try { return JSON.parse(string) } catch (e) { ctx.addIssue({ code: 'custom', message: 'Invalidate JSON' }) return z.never } }) ``` ## reference [官方文件](https://zod.dev/) ## arri https://github.com/modiimedia/arri ## arktype https://arktype.io/ ## Recursive types ```typescript const baseCategorySchema = z.object({ name: z.string(), }); type Category = z.infer<typeof baseCategorySchema> & { subcategories: Category[] } const categorySchema: z.ZodType<Category> = baseCategorySchema.extend({ subcategories: z.lazy(() => categorySchema.array()) }) const result = categorySchema.parse({ name: "People", subcategories: [ { name: "Politicians", subcategories: [ { name: "Presidents", subcategories: [], }, ], }, ], }); ``` ```typescript const isValidId = (id: string): id is `${string}/${string}` => id.split("/").length === 2 const baseCategorySchema = z.object({ id: z.string().refine(isValidId), }); type Input = z.input<typeof baseCategorySchema> & { children: Input[] | null; } type Output = z.output<typeof baseCategorySchema> & { children: Output[] | null; } const schema: z.ZodType<Output, z.ZodTypeDef, Input> = baseCategorySchema.extend({ children: z.lazy(() => schema.array().nullable()) }) const result = schema.parse({ id: '123/11', children: [{ id: '1/2', children: null }] }) ``` ## zod object key type ```typescript export const MapGMTtoTimezone = { 'GMT-12': 'Etc/GMT+12', 'GMT-11': 'Pacific/Midway', 'GMT-10': 'Pacific/Honolulu', 'GMT-9': 'Pacific/Gambier', 'GMT-8': 'America/Anchorage', //... } as const; const zodEnumFromObjectKeys = <Key extends string>(object: Record<Key, any>) => { const [firstKeys, ...anotherKeys] = Object.keys(object) as Key[]; return z.enum([firstKeys, ...anotherKeys]); }; const timezoneSchema = zodEnumFromObjectKeys(MapGMTtoTimezone); type TimeZone = z.infer<typeof timezoneSchema>; // "GMT-12" | "GMT-11" | ... ``` ## password schema ```typescript const passwordSchema = z .string() .min(8, { message: 'Password must be at least 8 characters long' }) .max(16, { message: 'Password must be at most 16 characters long' }) .refine((password) => /[A-Z]/.test(password), { message: 'Password must contain at least one uppercase letter' }) .refine((password) => /[a-z]/.test(password), { message: 'Password must contain at least one lowercase letter' }) .refine((password) => /[0-9]/.test(password), { message: 'Password must contain at least one number' }) .refine((password) => /[\W|_]/.test(password), { message: 'Password must contain at least one special character' }) .refine((password) => /^[^\s.]+$/.test(password), { message: 'Password must not contain spaces or dots' }); const updatePasswordSchema = z.object({ currentPassword: z.string(), password: passwordSchema, confirmPassword: z.string(), }).refine(({ confirmPassword, password }) => password === confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'] }) ``` ## auto type infer ```typescript // 自動擴充但可讀性不高 type Color = 'blue' | 'green' | 'red' type Variant = 'soft' | 'subtitle' | 'overLay' type ButtonType = | `${Extract<Variant, 'soft'>}-${Extract<Color, 'overLay'>}` | `${Exclude<Variant, 'soft'>}-${Exclude<Color, 'overLay'>}` // KISS (Keep It Simple Stupid) 原則,缺點是要一個一個列舉 type ButtonType2 = | "subtitle-blue" | "subtitle-green" | "subtitle-red" | "overLay-blue" | "overLay-green" | "overLay-red" ``` ## discriminatedUnion 單然你也可以使用 `unions` 但效能會差很多因為 `unions` 他會遍歷所有的 `key` ,但使用 `discriminatedUnion` 只會針對需要 `discriminated` 的 `key` 去做 `validate` ```typescript import z from "zod"; const schema = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.string() }), z.object({ status: z.literal("failed"), error: z.string() }), ]); type Result = z.infer<typeof schema>; const handleResult = (result: Result) => { if (result.status === "success") { return result.data; } return result.error; } ``` 另外一個好處是 `discriminatedUnion` 他可以做 `nesting` ```typescript const BaseError = { status: z.literal("failed"), message: z.string() }; const MyErrors = z.discriminatedUnion("code", [ z.object({ ...BaseError, code: z.literal(400) }), z.object({ ...BaseError, code: z.literal(401) }), z.object({ ...BaseError, code: z.literal(500) }), ]); const MyResult = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.string() }), MyErrors ]); ```