
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


