概要
最近、TypeScriptのバリデーションライブラリとしてArkTypeが話題になっているのを目にするようになってきました。
定番として使われているのはZodだと思うのですが、ArkTypeにはどのような特徴があるのか気になったので調べてみました。
結論は、ArkTypeは「程々に軽く、結構速く、TypeScriptの書き方に似ていて書きやすい」というバランスが良い点かと思いました。
比較対象
以下の3つを比べます。
- 素のTypeScript 5.9.3
- Zod 4.1.13
- ArkType 2.1.29
検証環境
今回の検証では、ユーザー登録フォームを3つの方式で実装し、コードの書きやすさ、エラーハンドリング、パフォーマンスを比較しました。
検証リポジトリ: https://github.com/kskwtnk/qiita-arktype
フォーム仕様
- name: 必須、2-50文字の文字列
- email: 必須、メールアドレスの形式
- password: 必須、8文字以上、英数字記号を含む
- age: 任意、18-120の数値
実装比較
素のTypeScript
TypeScriptの型定義とバリデーション関数を手動で実装します。
type User = {
name: string;
email: string;
password: string;
age?: number;
};
function validateUser(user: Partial<User>): ValidationErrors {
const errors: ValidationErrors = {};
if (!user.name) {
errors.name = '名前は必須です';
} else if (user.name.length < 2 || user.name.length > 50) {
errors.name = '名前は2文字以上50文字以内で入力してください';
}
if (!user.email) {
errors.email = 'メールアドレスは必須です';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) {
errors.email = '有効なメールアドレスを入力してください';
}
if (!user.password) {
errors.password = 'パスワードは必須です';
} else if (user.password.length < 8) {
errors.password = 'パスワードは8文字以上で入力してください';
}
return errors;
}
メリット
- 外部依存がない
- すべてを自分でコントロールできる
- バンドルサイズへの影響がない
デメリット
- コード量が多くなりがち
- バリデーションロジックの重複が発生しやすい
- エラーハンドリングを自分で実装する必要がある
Zod
Zodのスキーマを使ってバリデーションを実装します。
import { z } from 'zod';
const userSchema = z.object({
name: z
.string()
.min(2, { message: '名前は2文字以上で入力してください' })
.max(50, { message: '名前は50文字以内で入力してください' }),
email: z
.email({ message: '有効なメールアドレスを入力してください' }),
password: z
.string()
.min(8, { message: 'パスワードは8文字以上で入力してください' })
.regex(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]/, {
message: 'パスワードは英数字と記号を含む必要があります',
}),
age: z
.number()
.min(18, { message: '年齢は18歳以上で入力してください' })
.max(120, { message: '年齢は120歳以下で入力してください' })
.optional(),
});
type User = z.infer<typeof userSchema>;
// 使用例
const result = userSchema.safeParse(formData);
if (!result.success) {
// エラーハンドリング
const fieldErrors: ValidationErrors = {};
result.error.issues.forEach((error) => {
const path = error.path[0] as keyof User;
fieldErrors[path] = error.message;
});
}
メリット
- 宣言的で読みやすいAPI
- カスタムエラーメッセージが簡単に実装できる
- 豊富なバリデーションメソッド(
.email()、.min()など) - 大規模なエコシステムとコミュニティ
デメリット
- バンドルサイズが約60KB
- パフォーマンスは中程度
ArkType
ArkTypeのTypeScriptライクな構文でバリデーションを実装します。
import { type } from 'arktype';
const userSchema = type({
name: '2 <= string <= 50',
email: 'string.email',
// 正規表現も定義文字列内に直接記述可能
password: 'string >= 8 & /^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]/',
'age?': '18 <= number <= 120',
});
type User = typeof userSchema.infer;
// 使用例
const result = userSchema(formData);
if (result instanceof type.errors) {
// エラーハンドリング
}
メリット
- 素のTypeScriptに近い高速なバリデーション
- TypeScriptライクな構文(
'2 <= string <= 50')で学習コストが低い - 範囲バリデーションや正規表現(
'string & /.../')を簡潔に記述できる
デメリット
- バンドルサイズが約50KB
- エコシステムがZodより小さい
- カスタムエラーメッセージの実装がZod(メソッドチェーンに引数を渡すだけ)に比べるとやや手間がかかる
パフォーマンス比較
1000件のユーザーデータを連続でバリデーションした結果です。
| 方式 | 実行時間 (ms) | 相対速度 | バンドルサイズ |
|---|---|---|---|
| 素のTypeScript | 2.5 | 1.00x | 0KB(追加無し) |
| ArkType | 3.2 | 1.28x | ~50KB |
| Zod | 8.5 | 3.40x | ~60KB |
素のTypeScriptが最速ですが、ArkTypeはそれに近い速さを記録しました。
バンドルサイズの比較
| ライブラリ | Minified | Minified + Gzipped |
|---|---|---|
| 素のTypeScript | 0KB(追加無し) | 0KB(追加無し) |
| ArkType | 154KB | 47.6KB |
| Zod | 287KB | 57.5KB |
Minified状態ではZodがArkTypeの約1.9倍、Gzipped状態では約1.2倍のサイズです。
この差が実用上どの程度の影響があるかはプロジェクト次第ですが、バンドルサイズを気にしつつも外部ライブラリの恩恵を受けたい場合、ArkTypeは良い選択肢となりそうです。
ちなみに両ライブラリともTree-shakingに対応しています。
構文の比較
範囲バリデーション
ArkTypeは範囲バリデーションの記述が読みやすいと思います。
// ArkType
const schema = type({
name: '2 <= string <= 50',
age: '18 <= number <= 120',
});
// Zod
const schema = z.object({
name: z.string().min(2).max(50),
age: z.number().min(18).max(120),
});
// 素のTypeScript
function validate(data: User) {
if (data.name.length < 2 || data.name.length > 50) {
// エラー
}
}
オプショナルフィールド
オプショナルについてはどれもそんなに変わらないというか、好みの問題な気もします。
// ArkType: '?'をキーに付与
const schema = type({
'age?': 'number',
});
// Zod: .optional()メソッド
const schema = z.object({
age: z.number().optional(),
});
// 素のTypeScript: 型定義で'?'を使用
type User = {
age?: number;
};
カスタムエラーメッセージ
カスタムエラーメッセージの実装はZodが一番簡単です。
// ArkType: エラーオブジェクトの解析または.narrow()による実装
// 方法1: エラーサマリーを解析
function formatArkTypeErrors(errors: any): ValidationErrors {
const formattedErrors: ValidationErrors = {};
if (errors.summary.includes('email')) {
formattedErrors.email = 'メールアドレスが無効です';
}
return formattedErrors;
}
// 方法2: .narrow()を使った詳細な制御(より堅牢)
const schema = type({ email: 'string.email' })
.narrow((data, ctx) => {
if (!isValid(data)) {
return ctx.reject({ message: 'カスタムエラーメッセージ' });
}
return true;
});
// Zod: 各メソッドに直接指定
const schema = z.object({
email: z.email({ message: 'メールアドレスが無効です' }),
});
選択基準
ArkTypeを選ぶべきのが良さそう
- パフォーマンスとバンドルサイズの両方を気にしたい
- 基本的なバリデーションで十分(複雑なカスタムロジックは少なめ)
- TypeScriptライクな構文で学習コストを抑えたい
Zodを選ぶのが良さそう
- 複雑なバリデーションロジックが多い
- カスタムエラーメッセージを簡単に実装したい
- エコシステム(tRPC、React Hook Formなど)との統合を重視したい
素のTypeScriptを選ぶのが良さそう
- とてもシンプルなバリデーション
- バンドルサイズが最優先
- 外部依存を避けたい
まとめ
最近話題になっているArkTypeを、素のTypeScriptとZodと比較して実装してみました。
検証で分かったこと
ArkTypeは以下の点でバランスの取れたライブラリでした。
- パフォーマンス: 素のTypeScriptに近い速さ(Zodの約2.7倍速い)
- バンドルサイズ: Zodより約20%小さい(gzip後で約10KB削減)
- 構文の親しみやすさ: TypeScript開発者なら即座に理解できる
自分の中では、構文が素のTypeScriptに近いのが一番嬉しいです。
Zodに慣れていないときは「これってどうやって表現するんだっけ……」とドキュメントを読んでいる時間が長かったです(最近はそれもAIでだいぶ解消されているのですが)。
一方で、複雑なカスタムバリデーションが多いときなどはZodの方が書きやすい・親切だと感じました。
今Zodを使っている場所でわざわざ乗り換えるほどではないかもしれませんが、新しいプロジェクトなら採用してみても良いのかな?と思います。