Hoi hoi! 👋

我是 nyaomaru,一位主要使用 TypeScript 的前端工程師與 OSS 開發者。

當我們在 TypeScript 裡需要執行期驗證時,對話通常會變成這樣 👇

「我該用哪個函式庫?」

或者,乾脆自己寫 type guard?

我維護著 is-kit,所以我一直很在意這條界線。

而最近我開始思考:

我們是不是把其實在解決不同問題的函式庫拿來比較了?

例如,如果我只想要這樣:

if (isUser(input)) {
  input.name;
}

我真的需要一套 schema 系統嗎?

但如果我想要:

  • 結構化的驗證錯誤
  • 轉換
  • 解析
  • schema 中繼資料
  • 框架整合

那麼一個小小的布林型 guard 大概也不夠。

所以我沒有再問 「哪個 validator 最好?」,而是嘗試了另一種比較方式。

我用以下方式實作相同的 User 驗證:

接著我編譯它們、用相同的 fixture 測試、量測獨立瀏覽器 bundle 大小,並跑了一個小型的穩態執行時間基準測試。

最難的部分不是讓七個 validator 都能編譯。

而是讓七個 validator 的意義一致。

就算是「最多 100 個字元的字串」,在執行期也可能代表不同的契約。

所以,讓我們看看每個工具實際上帶來了什麼。😺

TypeScript Validation Battle


🧪 我們要驗證的 User

先來看共用型別。

type User = {
  id: string;
  name: string;
  age: number;
  role: "admin" | "user";
  tags: string[];
  profile: {
    bio: string;
    active: boolean;
  };
};

執行期規則如下:

  • id 必須是字串
  • name 必須包含 1–100 個 UTF-16 code unit
  • age 必須是介於 18 到 120 的整數
  • role 必須剛好是 "admin" 或 "user"
  • tags 必須是字串陣列
  • profile.bio 必須是字串
  • profile.active 必須是布林值
  • 根層與 profile 內都不接受額外 key
  • 值不做 coercion(型別轉換)

這次比較的主要輸入範圍是一般的 JSON 解析後資料。

這點很重要,因為 JavaScript 還可能給我們一些奇怪的東西,例如繼承屬性、稀疏陣列、symbol、getter,以及非可列舉 key。

我們稍後再回來談那些。


1. ✍️ 手寫 Guard — 有時候 Guard 就夠了

先從最樸素的選擇開始:手寫 JavaScript guard,再搭配 TypeScript predicate。

不用任何驗證函式庫。

const isRecord = (value: unknown): value is Record<string, unknown> =>
  typeof value === "object" && value !== null && !Array.isArray(value);

const hasExactKeys = (
  value: Record<string, unknown>,
  keys: readonly string[],
): boolean =>
  Object.keys(value).length === keys.length &&
  keys.every((key) => Object.hasOwn(value, key));

export const isUser = (value: unknown): value is User => {
  if (
    !isRecord(value) ||
    !hasExactKeys(value, ["id", "name", "age", "role", "tags", "profile"])
  ) {
    return false;
  }

  if (
    !isRecord(value.profile) ||
    !hasExactKeys(value.profile, ["bio", "active"])
  ) {
    return false;
  }

  return (
    typeof value.id === "string" &&
    typeof value.name === "string" &&
    value.name.length >= 1 &&
    value.name.length <= 100 &&
    typeof value.age === "number" &&
    Number.isInteger(value.age) &&
    value.age >= 18 &&
    value.age <= 120 &&
    (value.role === "admin" || value.role === "user") &&
    Array.isArray(value.tags) &&
    value.tags.every((tag) => typeof tag === "string") &&
    typeof value.profile.bio === "string" &&
    typeof value.profile.active === "boolean"
  );
};

然後:

declare const input: unknown;

if (isUser(input)) {
  input.name.toUpperCase();
}

這其實已經非常好了。

對於一個局部邊界,手寫 type guard 有很大的優勢:

不需要學額外的抽象模型。

它就是 JavaScript 的執行期檢查,加上 TypeScript predicate。

但當你的程式碼規模變成這樣時,成本就開始出現了:

isUser;
isProfile;
isRole;
isNonEmptyString;
isAdultAge;
isStringArray;
isApiResponse;
isConfig;
isFeatureFlag;

這時你寫的就不再是「一個 guard」了。

你是在維護你自己的小型 guard 函式庫。

而這就是下一個選項開始有意義的地方。👍


2. 🔧 is-kit — 當小型 Guards 變成可重用積木

這正是我圍繞 is-kit 所解決的問題。

我不想為每個 narrowing 問題都改用 schema-first 工作流程。

我想要的是小型 guard 保持小型,但能乾淨地組合。

import {
  and,
  arrayOf,
  isBoolean,
  isNumber,
  isString,
  oneOfValues,
  predicateToRefine,
  safeParse,
  struct,
} from "is-kit";

export const isName = and(
  isString,
  predicateToRefine<string>(
    (value) => value.length >= 1 && value.length <= 100,
  ),
);

export const isAge = and(
  isNumber,
  predicateToRefine<number>(
    (value) => Number.isInteger(value) && value >= 18 && value <= 120,
  ),
);

export const isProfile = struct(
  {
    bio: isString,
    active: isBoolean,
  },
  { exact: true },
);

export const isUser = struct(
  {
    id: isString,
    name: isName,
    age: isAge,
    role: oneOfValues("admin", "user"),
    tags: arrayOf(isString),
    profile: isProfile,
  },
  { exact: true },
);

export const parseUser = (input: unknown) => safeParse(isUser, input);

重點是:這些片段本身仍然是普通 guard。

const names = values.filter(isName);

if (isAge(value)) {
  // value 是 number
}

當你的程式碼本來就是以 predicate 思考時,這很有用。

而這也是我會劃下界線的地方。

safeParse() 只是替 guard 包上一層簡單的成功/失敗結果。

它不會讓 is-kit 變成:

  • 欄位層級的 issue 系統
  • 轉換管線
  • schema 中繼資料系統
  • OpenAPI 生態系
  • 表單整合層

如果那些才是你的真正需求,我寧可直接用 schema 函式庫,也不會硬把 guard 工具箱改造成 schema。

另外一個在驗證過程中浮現的型別細節是:struct() 和 arrayOf() 推斷出來的是 readonly 結構。

所以 runtime 形狀雖然和我們的 User 相符,但 readonly string[] 不能直接指派給可變的 string[]。

這個差異是真實存在的,而且 type tests 會把它明確呈現出來,而不是偷偷用 cast 掩蓋掉。🔧


3. 🏃 Runtypes — 不只是 predicate,而是 validator 物件

Runtypes 再往上一層。

它不只是布林 predicate,而是一個 runtime validator 物件,可以用多種方式消費。

import {
  Array as RtArray,
  Boolean as RtBoolean,
  Literal,
  Number as RtNumber,
  Object as RtObject,
  String as RtString,
  Union,
} from "runtypes";

const Name = RtString.withConstraint(
  (value) =>
    (value.length >= 1 && value.length <= 100) || "name length must be 1..100",
);

const Age = RtNumber.withConstraint(
  (value) =>
    (Number.isInteger(value) && value >= 18 && value <= 120) ||
    "age must be an integer in 18..120",
);

export const UserSchema = RtObject({
  id: RtString,
  name: Name,
  age: Age,
  role: Union(Literal("admin"), Literal("user")),
  tags: RtArray(RtString),
  profile: RtObject({
    bio: RtString,
    active: RtBoolean,
  }).exact(),
}).exact();

export const isUser = (input: unknown) => UserSchema.guard(input);

export const checkUser = (input: unknown) => UserSchema.check(input);

export const parseUser = (input: unknown) => UserSchema.parse(input);

export const inspectUser = (input: unknown) => UserSchema.inspect(input);

這和 guard 工具箱的感覺不太一樣。

predicate 主要回答的是:

這個值是不是 User?

Runtype 也能回答這個問題,但同一個物件還能:

  • 用 guard 做 narrowing
  • 用 check 丟出例外
  • 用 parse 解析
  • 提供結構化失敗資料

另外,check() 和 parse() 不能被視為同義詞。

Runtypes 支援 parser 行為,而物件處理在驗證與解析情境中可能不同。

這正是純 predicate 沒有的執行期機制。🏃


4. 🤖 Valibot — 具有模組化 API 的 Schema 驗證

Valibot 的比較開始更像「schema」了。

import * as v from "valibot";

export const UserSchema = v.strictObject({
  id: v.string(),
  name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
  age: v.pipe(v.number(), v.integer(), v.minValue(18), v.maxValue(120)),
  role: v.picklist(["admin", "user"]),
  tags: v.array(v.string()),
  profile: v.strictObject({
    bio: v.string(),
    active: v.boolean(),
  }),
});

export const isUser = (input: unknown) => v.is(UserSchema, input);

export const parseUser = (input: unknown) => v.safeParse(UserSchema, input);

這讓我們得到:

  • schema 驗證
  • 型別推斷
  • 結構化問題資訊
  • 解析 API
  • 轉換管線

但 API 仍然是以函式為主,而且是模組化的。

這是 Valibot 設計的重要一部分。

不是所有能力都掛在大型 schema 物件的方法上,而是透過彼此獨立的函式來組合。

這讓 tree shaking 成為預設設計的一部分。

而且和純粹的 schema-only 思維不同,Valibot 也提供 v.is() 作為 type guard。

所以 schema 驗證與一般 TypeScript narrowing 並不是互斥的。

不過有一點要注意:v.is() 是 guard 路徑,它不會像 parse 那樣提供驗證錯誤,也不會套用轉換。🤖


5. 💎 Zod — 當 Schema 成為更大生態系的一部分

接著是 Zod。

import * as z from "zod";

export const UserSchema = z.strictObject({
  id: z.string(),
  name: z
    .string()
    .refine(
      (value) => value.length >= 1 && value.length <= 100,
      "name must contain 1..100 UTF-16 code units",
    ),
  age: z.int().min(18).max(120),
  role: z.enum(["admin", "user"]),
  tags: z.array(z.string()),
  profile: z.strictObject({
    bio: z.string(),
    active: z.boolean(),
  }),
});

export type User = z.output<typeof UserSchema>;

export const parseUser = (input: unknown) => UserSchema.safeParse(input);

// 這個 schema 沒有轉換,所以它的輸出型別也可以安全地用於輸入。
export const isUser = (input: unknown): input is User => parseUser(input).success;

Zod 沒有內建 is() 方法,但這個 adapter 可以讓 input 縮窄,因為這個 schema 沒有做轉換。若是有轉換的 schema,safeParse().data 才是安全且有型別的值:原始輸入型別與輸出型別可能不同。

Zod 的價值不在於它能驗證 User。

這些函式庫都辦得到。

真正的價值在於 schema 周邊的整個生態:

  • 表單整合
  • API 工具
  • schema 轉換
  • 中繼資料
  • codec
  • OpenAPI 相關工具
  • 框架整合

如果你的 schema 已經開始變成應用基礎架構的一部分,那這個生態系就很重要。💎

不過這個範例有一個重要細節。

我沒有用這個:

z.string().min(1).max(100);

為什麼?

因為我們的契約寫的是:

1–100 個 UTF-16 code unit

Zod 內建的字串長度檢查使用的是 Unicode code point,所以這個範例改用 .refine() 搭配 JavaScript 的 string.length,以保留前述的 UTF-16 契約。下面的共同 emoji fixture 會示範為什麼如此。

那 bundle size 呢?

太容易直接寫成:

Valibot 很小,Zod 很大。

但這在 2026 年不是很好的比較方式了。

Zod 也有 Zod Mini,使用更函式化、可 tree-shake 的 API。

這篇文章後面的 bundle 表格量測的是一般版 Zod,不是 Zod Mini。

所以它不應該被解讀成 Valibot 與「最小化設定的 Zod」之間的比較。

Zod 也可以在執行期用 z.compile() 和 new Function 編譯出最佳化的驗證邏輯。

這裡我沒有啟用它。

因此,這裡的區分比「生成式驗證」這個大標籤更有意義:

執行期 schema 編譯
  vs
從 TypeScript 型別在 build 時產生程式碼

而這個區別在我們談到 typia 時就很重要。


6. 🚢 ArkType — 如果 runtime schema 長得像 TypeScript 會怎樣?

ArkType 的撰寫模型很不一樣。

import { type } from "arktype";

export const UserSchema = type({
  id: "string",
  name: "0 < string <= 100",
  age: "18 <= number.integer <= 120",
  role: "'admin' | 'user'",
  tags: "string[]",
  profile: {
    bio: "string",
    active: "boolean",
  },
}).onDeepUndeclaredKey("reject");

export const isUser = (input: unknown) => UserSchema.allows(input);

export const parseUser = (input: unknown) => UserSchema(input);

export const ArkErrors = type.errors;

我很喜歡這種比較:

z.int().min(18).max(120);

對上

"18 <= number.integer <= 120";

沒有哪個「比較好」。

它們只是表達執行期契約的不同方式。

如果你的團隊本來就習慣 TypeScript 風格的表達式,ArkType 會非常直觀。

allows() 也可以當作 type guard。

而直接呼叫 schema 則會回傳驗證後的輸出或錯誤集合。

還有一個有趣的點:當環境允許 new Function 時,ArkType 可以在執行期預先編譯出最佳化的 traversal 邏輯。

所以 ArkType 本身就已經在做 specialization。🚢

但這個架構仍然和 typia 不同。

ArkType 是從 runtime 的 type representation 出發。

typia 則是在 build 階段從 TypeScript 型別出發。


7. 🚀 typia — 在 build 階段生成 validator

typia 在這次比較中是異類。

import typia, { type tags } from "typia";

type Utf16Name = tags.TagBase<{
  kind: "utf16Name";
  target: "string";
  value: undefined;
  validate: "$input.length >= 1 && $input.length <= 100";
}>;

export interface User {
  id: string;
  name: string & Utf16Name;
  age: number & tags.Type<"uint32"> & tags.Minimum<18> & tags.Maximum<120>;
  role: "admin" | "user";
  tags: string[];
  profile: {
    bio: string;
    active: boolean;
  };
}

export const isUser = typia.createEquals<User>();

export const validateUser = typia.createValidateEquals<User>();

這不是一個 runtime schema 物件。

來源真相是 TypeScript 型別,再加上 typia 的 tags。

接著 typia 的 build transform 會把:

typia.createEquals<User>();

轉換成專門化的驗證程式碼。

這代表不會有一個通用的 runtime schema interpreter 在那裡走訪 User schema 物件。

但這也有取捨:

你需要 build 整合。

目前官方設定使用 TypeScript 7 與 ttsc / typia 的 transform 整合。

單靠原生 tsc 不會執行這個 transform。

在我的驗證工作區中,第一次建立 typia 時還必須編譯一個 Go 原始碼外掛,整體花了幾分鐘。

這個設定成本不算在 runtime benchmark 裡,但絕對是採用決策的一部分。

另外,typia 內建的 MinLength / MaxLength 不符合我們的 UTF-16 契約。

所以我為 name 使用了自訂 tag。

同樣看起來像是相同的驗證規則,但語意其實不同。🚀


😹 等一下,「字串長度」居然也不一樣?

這是第一個讓我停下來的測試。

"😀".repeat(51).length;
// 102

JavaScript 的 string.length 計算的是 UTF-16 code unit。

所以依照我們的契約:

最大值 = 100 個 UTF-16 code unit

這個應該失敗。

而且它確實在以下實作中失敗了:

  • 手寫 guard
  • is-kit
  • Runtypes
  • Valibot
  • ArkType

但原本的 Zod 和 typia 範例卻接受了它。

為什麼?

因為它們內建的長度限制是以 Unicode code point 計算。

對於這個字串:

51 個 emoji
= 51 個 code point
= 102 個 UTF-16 code unit

兩種行為都沒有錯。

它們只是不同的契約。

而且它們也不一定等於「人眼看到的字數」,因為 grapheme cluster 會讓 Unicode 更有趣。😹

所以我把 Zod 改成 .refine(),把 typia 改成自訂 tag。

這大概是整個比較裡最有價值的一課 👇

看起來相同的 schema,不代表 runtime 契約一定相同。


那我們其實在比較什麼?

到了這裡,這些工具已經不再像七個可互換的函式庫。

它們是不同的 runtime 模型。

方式 真實來源 最自然的成功 API 結構化失敗資訊 轉換模型 編譯 / build 需求
手寫 guard 型別 + 手寫檢查 Predicate 手動處理 手動處理 無
is-kit 組合式 guards Predicate / safeParse 基本的 valid/invalid 結果 以 guard 為中心 無
Runtypes Runtype 物件 guard / check / parse 有 Parsers 無
Valibot Schema 函式 is / safeParse 有 Schema pipelines 無
Zod Schema 物件 parse / safeParse 有 Transforms / codecs 無 build transform;可選執行期 z.compile()
ArkType 執行期型別表達式 allows / 直接呼叫 有 Morphs 無 build transform;可在執行期預編譯(若可用)
typia TypeScript 型別與 tags 生成的 predicates / 驗證結果 搭配驗證 API build 期間生成函式 需要 build transform

這也是為什麼我不再喜歡把它們想成單純的升級階梯。

schema 函式庫不代表「更好的 type guard」。
生成式 validator 也不代表「更好的 schema」。

它們是在不同層級解決重疊的問題。


✅ 真的通過了哪些?

完整的 harness、lockfile、fixtures、探針與產生出的結果檔都放在 typescript-validator-comparison。文章連結的是那個不可變的 commit,而不是會變動的分支。

若要重現當時的快照:

git clone https://github.com/nyaomaru/typescript-validator-comparison.git
cd typescript-validator-comparison
git checkout --detach b26350d01fd18cb77d55a95e2f3b112657f0d305
corepack pnpm install --frozen-lockfile
corepack pnpm verify

pnpm verify 會執行 lint、strict type checking、fixtures、bundle 量測、benchmarks、純 JavaScript 探針,以及報告生成。下面的結果表就是那次執行保存下來的快照;在別台機器上重新執行,自然會得到不同的時間數字。

我鎖定版本並用相同的 fixture matrix 測試所有七個實作。

環境

Date:     2026-10-23
Node:     24.22.1
TypeScript: 7.0.2
pnpm:     11.22.0
ttsc:     0.31.3
tsbuild:  0.28.2

套件

套件 版本
is-kit 1.15.3
runtypes 7.0.5
valibot 1.5.0
zod 4.6.5
arktype 2.2.8
typia 15.1.0

共用 matrix 包含 35 個 JSON fixture。

其中包含:

  • 有效的 user
  • age 邊界
  • name 邊界
  • emoji 長度案例
  • 錯誤的原始型別
  • 缺少欄位
  • 無效 role
  • 無效陣列元素
  • 額外的根層 key
  • 額外的巢狀 key
  • 非物件根值

七個實作都符合預期結果。

35 fixtures
× 7 validators
= 245 boolean checks

這 245 次全部通過。

這個 harness 也會檢查 input mutation、更豐富的 API、嚴格的 TypeScript 編譯、lint、build、narrowing,以及 bundle 輸出行為。

所以這些實作對於本文中的 JSON 形狀契約是一致的——但不一定對所有 JavaScript 值都一致。


🧩 另一個驚喜:「Exact」不一定真的 exact

我也測試了正常 JSON 形狀輸入範圍以外的值。表格中的四個探針意思如下:

const sparseTags = { ...valid, tags: new Array(2) };
const inheritedFields = Object.create(valid);
const extraSymbol = { ...valid, [Symbol("extra")]: true };
const extraUndefined = { ...valid, extra: undefined };

倉庫另外還有九個探針,包括非可列舉屬性與會拋錯的 getter。以下是這四個的結果:

探針 Native is-kit Runtypes Valibot Zod ArkType typia
稀疏 tags 陣列 接受 拒絕 接受 拒絕 拒絕 拒絕 接受
所有欄位皆為繼承屬性 拒絕 拒絕 拒絕 接受 接受 接受 接受
額外的 symbol 屬性 接受 接受 拒絕 接受 接受 拒絕 接受
額外 key,值為 undefined 拒絕 拒絕 拒絕 拒絕 拒絕 拒絕 接受

我測試的那個額外非可列舉屬性,七個都接受。

遇到會拋錯的 getter 時,七個都會丟出例外。

這不代表某個函式庫「正確」,而其他都壞掉。

它代表的是:

「exact object validation」必須先定義範圍。

例如,我的原生 guard 使用的是:

Object.keys(value);

它只會看到可列舉的字串 key。

而這段:

value.tags.every(...)

會跳過稀疏陣列中的洞。

對 JSON 資料來說這完全合理。

但它並不是一條適用於 JavaScript 所有奇怪物件的通用政策。

這也是我認為 validator 比較應該先說明預期輸入範圍的原因。


📦 Bundle 大小

接著是有趣的表格。😺

每個瀏覽器目標的 entry 都只輸出 validate。

它們都用相同的 esbuild 設定打包:

  • browser platform
  • ESM
  • ES2022
  • minification
  • tree shaking
  • 移除 legal comments

然後我量測原始大小、gzip 與 Brotli 輸出。

Validator Raw bytes gzip bytes Brotli bytes
手寫 guard 588 337 295
is-kit 3,421 1,530 1,423
runtypes 16,667 5,188 4,657
valibot 5,722 1,853 1,675
zod 89,888 26,212 23,331
arktype 155,549 47,603 41,956
typia 772 403 339

幾個重要的注意事項:

這是獨立成本

如果你的 app 已經在用 Zod,再多一個 schema 的邊際成本不會是 26 KB gzip。

這張表量的是獨立 entry bundle。

這是一般版 Zod

不是 Zod Mini。

所以這張表不能回答:

Valibot vs Zod Mini?

生成式不等於零位元組

typia 在這個測試中的最終 guard bundle 只有 772 bytes raw。

但它仍然包含生成出來的程式碼和一個 uint32 helper。

「生成式 validator」不代表「免費 validator」。

不過,架構上的差異在這裡非常明顯。


⚡ Runtime

我原本差點不放 runtime 數字,因為 validator benchmark 很容易誤導人。
但既然這個工作區已經有相同契約和 fixtures,我還是跑了一個小型穩態 benchmark。

三種情境:

  • valid user
  • 錯誤的 id
  • 錯誤的巢狀 profile.active

每個函式庫都在獨立的 Node process 中執行。

每種情境使用:

  • 20,000 次 warmup
  • 9 組測量樣本
  • 每組 50,000 次呼叫

schema 和 validator 都在量測迴圈外建立。

結果是每次呼叫的中位數奈秒數,括號內為最小值到最大值。

Validator Valid Wrong id Wrong profile.active
手寫 guard 79.9 (77.3–114.1) 73.6 (72.1–84.9) 80.8 (79.0–96.3)
is-kit 232.0 (225.7–257.5) 34.5 (33.1–38.9) 210.3 (203.6–218.1)
runtypes 5804.0 (5425.5–6298.0) 9221.8 (8443.2–11063.2) 10101.2 (9287.6–10503.5)
valibot 951.2 (425.2–1782.7) 200.2 (96.1–422.3) 1112.1 (648.4–1538.1)
zod 612.6 (351.3–1203.5) 1491.6 (862.9–2824.6) 2208.5 (1592.4–4757.4)
arktype 291.5 (278.1–614.4) 9.6 (6.6–17.7) 12.0 (11.3–37.6)
typia 21.9 (20.5–46.1) 2.0 (1.9–6.4) 11.5 (10.8–17.2)

請不要把這解讀成:

typia 是最快的 validator。

這是微基準測試。

相同的小物件形狀會被反覆驗證,V8 會非常積極地最佳化。

我不會把 2.0 ns 當成真實應用中「驗證成本」的可移植數字。

成功 API 也不同:

  • 手寫 guard 使用 predicate
  • is-kit 使用 predicate
  • Runtypes 使用 .guard()
  • Valibot 使用 is()
  • Zod 使用 safeParse(...).success
  • ArkType 使用 allows()
  • typia 使用生成的 equals guard

這些操作並不全都做完全相同的事。

這個 benchmark 真正告訴我們的是:架構和失敗路徑都很重要。

例如:

  • 手寫 guard 在拒絕錯誤 id 之前,先付出了 exact-key 檢查的成本
  • ArkType 的無效路徑比有效路徑便宜很多
  • typia 生成的 guard 在這裡的穩態開銷非常低
  • Runtypes 在這個熱迴圈中的較豐富驗證機制成本比較高

這些資訊是有用的。

但不要把它當成通用速度排名。


那麼……我會用哪個?🤔

做完這些之後,我的決策樹其實很簡單。

只是單一、局部的 narrowing 檢查?

用手寫 guard。

if (isUser(input)) {
  // input 是 User
}

當問題很小時,沒有依賴本身就是一種優勢。

很多可重用的 guards?

這時我會用 is-kit。🔧

values.filter(isName);

if (isAge(value)) {
  // ...
}

問題不是「我需要 schema」。

問題是「我一直在重寫相同的 predicates 和組合輔助函式」。

想要一個 validator 物件?

如果你喜歡有一個 runtime 物件,同時可以做:🏃

guard
check
parse
inspect
assert

那 Runtypes 很值得注意。

想要模組化 API 的 schema 驗證?

Valibot。🤖

特別是當 client bundle 大小與 tree shaking 是設計重點時。

想要 schema 與更大的生態系綁在一起?

Zod。💎

在很多應用裡,這大概就是它最強的存在理由。

如果你在意 bundle size,請另外評估 Zod Mini。

想讓 schema 語法像 TypeScript?

ArkType。🚢

"18 <= number.integer <= 120";

這和鏈式 schema 方法是完全不同的撰寫體驗。

想把 TypeScript 型別在 build 階段轉成 validator?

typia。
但我會把 build 整合視為決策的一部分,而不是註腳。


最後的想法

這些工具彼此重疊,但它們位於不同層級。

難的不是讓七個 validator 都能編譯,而是讓七個 validator 的意義一致。

所以在比較套件大小或 benchmark 截圖之前,先問問看你的邊界到底需要回傳什麼,以及值不值得帶上哪些 runtime 機制。

用最小、但仍能充分描述你的邊界的模型。

那麼,你會用哪一個呢?

感謝閱讀!🤲


原文出處:https://dev.to/nyaomaru/i-compared-6-typescript-validation-libraries-heres-what-surprised-me-325g


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝7   💬1  
269
🥈
boluoji
📝5   ❤️3
87
🥉
mike
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登