Zod: TypeScript Schema Validation library mạnh mẽ và type-safe

Zod: TypeScript Schema Validation library mạnh mẽ và type-safe

Zod là library validation schema cho TypeScript giúp developer đảm bảo dữ liệu đầu vào chính xác đồng thời giữ nguyên type information. Bài viết này giới thiệu cách sử dụng Zod trong dự án thực tế, các pattern phổ biến và tại sao nó đang trở thành tiêu chuẩn mới trong ecosystem TypeScript.

## Zod là gì và tại sao chọn Zod

Zod là một JavaScript/TypeScript schema validation library với ưu điểm chính:
– **Type inference tự động**: TypeScript type được suy ra trực tiếp từ schema, không cần định nghĩa type riêng
– **Tree-shakeable**: Chỉ export những gì cần dùng, bundle size nhỏ
– **Errors tiếng Anh dễ hiểu**: Validate errors rõ ràng và chi tiết
– **Custom validation linh hoạt**: Dễ dàng tạo custom validators và transformers
– **Không external dependencies**: Zod không phụ thuộc vào package nào khác

### So sánh với các library khác

| Library | Type Safety | Bundle Size | Learning Curve |
|———|————|———–|—————|
| **Zod** | TypeScript-native | ~14kb | Thấp |
| Yup | TypeScript support | ~32kb | Thấp |
| Joi | Cần @types/joi | ~140kb (bundle lớn) | Thấp |
| Yup + TypeScript | Cần annotation | ~32kb | Trung bình |
| Superstruct | TypeScript-native | ~6kb | Trung bình |

## Cú pháp cơ bản

### Định nghĩa schema cơ bản

“`typescript
import { z } from ‘zod’;

// Schema cơ bản
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive(),
isActive: z.boolean().default(true),
tags: z.array(z.string()),
profile: z.object({
bio: z.string().optional(),
avatar: z.string().url().optional()
}).optional()
});

// Type inference
type User = z.infer;
// Tương đương với:
// type User = {
// id: string;
// name: string;
// email: string;
// age: number;
// isActive: boolean;
// tags: string[];
// profile?: { bio?: string; avatar?: string } | undefined;
// }
“`

### Validation và parsing

“`typescript
// Parse — throws nếu data không hợp lệ
try {
const user = UserSchema.parse({
id: ‘123e4567-e89b-12d3-a456-426614174000’,
name: ‘Nguyễn Văn A’,
email: ‘[email protected]’,
age: 25,
tags: [‘developer’]
});
console.log(user); // Full typed User object
} catch (error) {
if (error instanceof z.ZodError) {
console.error(error.errors);
}
}

// Safe parse — trả về result object, không throw
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data); // typed User
} else {
console.error(result.error); // ZodError
}
“`

## Các loại schema phổ biến

### Primitives và refinements

“`typescript
// String với constraints
const UsernameSchema = z.string()
.min(3, ‘Username phải có ít nhất 3 ký tự’)
.max(20, ‘Username không được quá 20 ký tự’)
.regex(/^[a-zA-Z0-9_]+$/, ‘Username chỉ chứa chữ, số và underscore’);

// Number với range
const RatingSchema = z.number()
.int()
.min(1, ‘Rating tối thiểu 1’)
.max(5, ‘Rating tối đa 5’);

// Enum và union
const StatusSchema = z.enum([‘active’, ‘inactive’, ‘pending’]);
const ResultSchema = z.union([z.string(), z.number()]);
“`

### Arrays và objects

“`typescript
// Array với validations
const TagsSchema = z.array(z.string().min(1))
.min(1, ‘Phải có ít nhất 1 tag’)
.max(10, ‘Tối đa 10 tags’);

// Nested objects
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
postalCode: z.string().regex(/^d{5,6}$/)
});

// Tùy chọn fields
const OptionalFieldsSchema = z.object({
requiredField: z.string(),
optionalField: z.string().optional(),
nullableField: z.string().nullable(),
defaultField: z.string().default(‘default value’)
});
“`

## Advanced patterns

### Discriminated unions

“`typescript
// Kiểu dữ kiện có determinant field
const EventSchema = z.discriminatedUnion(‘type’, [
z.object({
type: z.literal(‘click’),
x: z.number(),
y: z.number()
}),
z.object({
type: z.literal(‘keypress’),
key: z.string(),
modifiers: z.array(z.string())
}),
z.object({
type: z.literal(‘focus’),
target: z.string()
})
]);
“`

### Transform và coerce

“`typescript
// Transform data sau khi validate
const DateSchema = z.string().transform((str) => new Date(str));

// Coerce — convert input types
const CountSchema = z.coerce.number(); // ’42’ → 42

// Pipe — validate trước rồi transform
const UserIdSchema = z.string().pipe(
z.string().uuid(‘Invalid UUID format’)
).pipe(z.string().transform(id => id.toUpperCase()));
“`

### Custom validation

“`typescript
// Custom refinements
const PasswordSchema = z.string()
.min(8, ‘Password phải có ít nhất 8 ký tự’)
.refine(
(password) => /[A-Z]/.test(password),
‘Password phải có ít nhất 1 chữ hoa’
)
.refine(
(password) => /[0-9]/.test(password),
‘Password phải có ít nhất 1 số’
)
.refine(
(password) => /[^A-Za-z0-9]/.test(password),
‘Password phải có ít nhất 1 ký tự đặc biệt’
);

// Custom error messages với code
const AgeSchema = z.number().int().refine(
(age) => age >= 18,
{ message: ‘Phải đủ 18 tuổi’, code: ‘too_young’ }
);
“`

## Sử dụng trong Express.js middleware

“`typescript
// Middleware validation function
function validate(schema: z.ZodSchema) {
return (req, res, next) => {
const result = schema.safeParse({
body: req.body,
query: req.query,
params: req.params
});

if (!result.success) {
return res.status(400).json({
error: ‘Validation failed’,
details: result.error.format()
});
}

req.validated = result.data;
next();
};
}

// Schema cho route
const CreateUserSchema = z.object({
body: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive()
})
});

// Sử dụng
app.post(‘/users’, validate(CreateUserSchema), (req, res) => {
const { name, email, age } = req.validated.body;
// Everything is fully typed here
res.json({ message: `Created user ${name}` });
});
“`

## Zod trong React Hook Form

Zod tích hợp tốt với React Hook Form qua resolver:

“`typescript
import { zodResolver } from ‘@hookform/resolvers/zod’;

const FormSchema = z.object({
email: z.string().email(‘Email không hợp lệ’),
password: z.string().min(8, ‘Password tối thiểu 8 ký tự’),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: ‘Mật khẩu không khớp’,
path: [‘confirmPassword’]
});

// type-safe form
type FormData = z.infer;

function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(FormSchema)
});

const onSubmit = (data: FormData) => {
// data.password đã được validate và typed
console.log(data);
};

return (

{errors.email && {errors.email.message}}

);
}
“`

## Best practices

– **Định nghĩa schema ở gần data source**: Đặt schema ở API boundary, form handler, hoặc wherever data enters system
– **Sử dụng type inference**: Không cần định nghĩa TypeScript type riêng, để Zod tự suy ra
– **Custom error messages tiếng Việt**: Zod hỗ trợ error message tùy chỉnh, sử dụng tiếng Việt cho UX tốt hơn
– **Lazy evaluation cho circular schemas**: Sử dụng `z.lazy()` cho cấu trúc dữ liệu recursive
– **Schema composition**: Kết hợp nhiều schema nhỏ thành schema lớn để tái sử dụng

## Tài liệu tham khảo

Zod Documentation
Zod GitHub Repository
React Hook Form

Diagram showing Zod type inference flow from schema to TypeScript types
Comparison diagram of Zod vs other validation libraries bundle size and performance
Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Drizzle ORM: TypeScript ORM hiện đại cho PostgreSQL và MySQL

Drizzle ORM là một ORM hiện đại dành cho TypeScript, hỗ trợ PostgreSQL, MySQL, SQLite và các database serverless như Turso, Neon, Cloudflare D1. Với kích thước chỉ ~7.4KB gzipped,…

Xem thêm

Git Worktree: Quản lý nhiều branch đồng thời trên cùng repo

Git Worktree là gì Bạn đang phát triển tính năng feature/A, rồi hotfix cấp bách từ branch main chen ngang. Thông thường bạn phải stash, checkout, sau đó quay lại…

Xem thêm

Rust Async: Tìm hiểu Future, Pin, và Waker trong async/await

Rust Async: Tìm hiểu Future, Pin, và Waker trong async/await Rust async/await là một trong những tính năng mạnh mẽ nhất của ngôn ngữ, cho phép viết code không đồng…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất