# 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

### 用zod

**是不是代碼很簡潔呢**
## 表單驗證與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
]);
```