Làm chủ Valibot trong TypeScript: Hướng dẫn Schema Validation nhẹ và tối ưu Tree-shaking

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Sự chuyển dịch sang Modular Schema Validation

Trong nhiều năm, Zod đã thống trị hệ sinh thái TypeScript. Nó đáng tin cậy, thân thiện với lập trình viên và tích hợp mượt mà với hệ thống kiểu (type system). Tuy nhiên, khi các nhà phát triển ngày càng chú trọng vào các chỉ số hiệu năng như Core Web Vitals, kích thước bundle (bundle size) đã trở thành mối quan tâm hàng đầu.

Zod dựa trên một API “fluent” dạng class. Thiết kế này khiến việc tree-shaking trở nên khó khăn vì mọi phương thức (method) đều được gắn vào một đối tượng lớn duy nhất. Ngay cả khi bạn chỉ sử dụng một phần nhỏ tính năng, bạn thường vẫn phải gửi toàn bộ thư viện nặng 13.4kB (đã nén gzipped) đến người dùng.

Valibot mang đến một lộ trình tinh gọn hơn. Thay vì chuỗi các phương thức như z.string().email(), Valibot sử dụng các hàm độc lập, có thể kết hợp (composable). Kiến trúc functional này cho phép các công cụ đóng gói hiện đại như Vite, Webpack, hoặc Rollup thực hiện tree-shaking một cách chính xác. Nếu dự án của bạn chỉ validate chuỗi, bundle production của bạn sẽ chỉ bao gồm logic cho chuỗi. Tôi đã triển khai thư viện này trong các môi trường production nơi mỗi kilobyte đều quan trọng, và hiệu quả mang lại là tức thì và có thể đo lường được.

Cái giá của sự tiện lợi: Valibot đối đầu Zod

Tại sao Valibot lại thu hút được nhiều sự chú ý đến vậy? Câu trả lời nằm ở cách nó xử lý các dependency. Hầu hết các thư viện truyền thống sử dụng “Fluent API”, nơi các phương thức được liên kết với một đối tượng chính. Mặc dù điều này trông gọn gàng trong IDE, nhưng nó tạo ra một đồ thị phụ thuộc cứng nhắc mà các trình đóng gói (bundler) không dễ dàng tách rời.

Cách tiếp cận của Zod (Method Chaining)

import { z } from 'zod';

const schema = z.string().min(5).email();
// Đối tượng 'z' là một khối duy nhất. 
// Bundler của bạn không thể dễ dàng loại bỏ các phương thức mà bạn không gọi.

Cách tiếp cận của Valibot (Functional & Modular)

import { string, minLength, email, pipe, parse } from 'valibot';

const schema = pipe(string(), minLength(5), email());
// Chỉ những hàm cụ thể này mới xuất hiện trong file JavaScript cuối cùng của bạn.

Bằng cách chuyển sang các hàm độc lập, Valibot đạt được kích thước cực kỳ nhỏ gọn. Trong một form thực tế điển hình, việc chuyển từ Zod sang Valibot có thể giảm trọng lượng logic validation từ ~13kB xuống còn chưa đầy 1kB. Đó là mức giảm 90% kích thước mã cho cùng một chức năng chính xác.

Ưu và nhược điểm: Có đáng để chuyển đổi?

Chọn một thư viện đòi hỏi sự cân bằng giữa các yếu tố đánh đổi. Mặc dù tôi khuyên dùng Valibot cho các dự án nhạy cảm về hiệu năng, bạn nên xem xét cả hai mặt trước khi di chuyển toàn bộ codebase của mình.

Ưu điểm

  • Kích thước Bundle tối thiểu: Hiện tại đây là một trong những thư viện validation nhẹ nhất trên thị trường.
  • Tree-shaking chi tiết: Bạn chỉ trả phí dung lượng cho những tính năng bạn thực sự import.
  • Type Safety toàn diện: Hỗ trợ TypeScript tuyệt vời với khả năng tự động suy luận kiểu (type inference).
  • Dễ dàng mở rộng: Các quy tắc tùy chỉnh chỉ là những hàm đơn giản, giúp việc viết chúng dễ dàng hơn so với các plugin của Zod.

Nhược điểm

  • Hệ sinh thái đang phát triển: Nó thiếu một thư viện đồ sộ các tích hợp bên thứ ba mà Zod đã xây dựng trong nhiều năm qua.
  • Cú pháp dài dòng hơn: Một số lập trình viên thấy cú pháp pipe() lặp đi lặp lại nhiều hơn so với dot-chaining của Zod.
  • Độ ổn định API: Là một thư viện mới hơn, API của nó có những thay đổi thường xuyên hơn so với các lựa chọn thay thế đã trưởng thành.

Bắt đầu

Valibot hoạt động ở bất cứ đâu TypeScript chạy, từ Node.js và Bun đến React và Vue. Việc cài đặt là tiêu chuẩn trên tất cả các trình quản lý gói:

npm install valibot
# hoặc
pnpm add valibot

Để giữ mọi thứ sạch sẽ, tôi khuyên bạn nên tạo một thư mục /schemas. Điều này cho phép bạn chia sẻ logic validation giữa các form ở frontend và các route API ở backend mà không bị trùng lặp.

Hướng dẫn triển khai: Từ cơ bản đến nâng cao

Hãy xem cách Valibot xử lý dữ liệu thực tế, bắt đầu từ các chuỗi đơn giản đến các đối tượng phức tạp.

1. Validation cơ bản

Để validate một giá trị duy nhất, hãy định nghĩa schema của bạn và sử dụng hàm parse. Lưu ý rằng parse sẽ ném ra một ValiError nếu dữ liệu không hợp lệ.

import { string, minLength, pipe, parse } from 'valibot';

const passwordSchema = pipe(
  string(), 
  minLength(8, "Mật khẩu phải có ít nhất 8 ký tự")
);

try {
  const result = parse(passwordSchema, "my-secure-password");
  console.log("Thành công:", result);
} catch (error) {
  console.error(error.message);
}

2. Safe Parsing cho môi trường Production

Tôi hiếm khi sử dụng parse trong môi trường production vì việc bắt lỗi bằng try/catch có thể hơi cồng kềnh. Thay vào đó, hãy sử dụng safeParse. Nó trả về một đối tượng kết quả cho bạn biết chính xác điều gì đã xảy ra mà không làm dừng mã của bạn.

import { string, email, pipe, safeParse } from 'valibot';

const emailSchema = pipe(string(), email());
const result = safeParse(emailSchema, "not-an-email");

if (result.success) {
  console.log("Dữ liệu hợp lệ:", result.output);
} else {
  // Truy cập các vấn đề lỗi chi tiết tại đây
  console.log("Validation thất bại:", result.issues);
}

3. Validate các đối tượng phức tạp

Valibot tỏa sáng khi xử lý các phản hồi API lồng nhau. Bạn thậm chí có thể trích xuất các kiểu TypeScript trực tiếp từ schema của mình bằng cách sử dụng InferOutput.

import { object, string, number, pipe, minLength, email, type InferOutput } from 'valibot';

const UserProfileSchema = object({
  username: pipe(string(), minLength(3)),
  age: number(),
  email: pipe(string(), email())
});

type UserProfile = InferOutput<typeof UserProfileSchema>;

const data = {
  username: "dev_user",
  age: 30,
  email: "[email protected]"
};

4. Tích hợp với React Hook Form

Hầu hết các nhà phát triển frontend sử dụng Valibot bên trong một thư viện form. Bạn có thể kết nối nó với React Hook Form bằng gói resolver chính thức.

import { useForm } from 'react-hook-form';
import { valibotResolver } from '@hookform/resolvers/valibot';

function RegistrationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: valibotResolver(UserProfileSchema)
  });

  const onSubmit = (data) => console.log("Dữ liệu Form:", data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} />
      {errors.username && <p>{errors.username.message}</p>}
      <button type="submit">Đăng ký</button>
    </form>
  );
}

Lời kết về hiệu năng

Sức mạnh của Valibot chính là sự đơn giản. Khi tôi chuyển đổi một dự án quy mô trung bình từ Zod sang, chỉ số “First Load JS” trong Lighthouse đã cải thiện được vài điểm. Mặc dù 13KB của Zod có vẻ nhỏ, nhưng nó sẽ cộng dồn đáng kể khi kết hợp với các thư viện khác. Khả năng thu gọn xuống còn 1KB của Valibot khiến nó trở thành lựa chọn ưu việt cho phát triển web hiện đại, ưu tiên hiệu năng. Nếu bạn đang bắt đầu một dự án mới hôm nay, hãy chọn thư viện biết tôn trọng băng thông của người dùng.

Share: