はじめに
TypeScriptでバリデーションライブラリを選ぶとき、Zodが第一候補になることが多いと思います。個人的にはZodを長く使ってきたんですが、最近ArkTypeというライブラリを試してみて、かなり衝撃を受けました。
GitHubで7,400スター以上、MITライセンス、52人以上のコントリビューター。そして何より「Zodより20倍高速」という触れ込み。正直なところ、最初は「また大げさな宣伝か」と思っていたんですよ。でも実際にベンチマークを取ってみたら、本当に速かった。
30代になって思うのは、パフォーマンスは「あれば嬉しい」ではなく「必須」になってきているということ。特にAPIのバリデーションで毎秒何万リクエストを捌くような場面では、14ナノ秒でオブジェクト検証できるのは大きい。
ArkTypeとは
ArkTypeは「TypeScript's 1:1 validator, optimized from editor to runtime」を謳うランタイムバリデーションライブラリです。
開発元はarktypeioで、6年以上の歴史があります。JSONペイロードやフォーム入力など、外部から入ってくるデータをコードの境界で検証する用途に使えます。Zodと同じ立ち位置ですね。
特徴的なのは、TypeScript構文をそのまま文字列として書けること。型定義とバリデーションロジックが完全に一致するので、「型とスキーマの二重定義問題」から解放されます。
特徴・メリット
1. 圧倒的なパフォーマンス
これ、意外とバカにできない話なんですけど、公式ベンチマークによると:
- Zodより20倍高速
- Yupより2000倍高速
- Node v23.6.1でのオブジェクト検証: 14ナノ秒
コスパ的に、大量のデータを扱うAPIやリアルタイム処理では、この速度差が効いてきます。
2. TypeScript構文そのまま
個人的にはこれが一番のメリットだと思います。
const User = type({
name: "string",
platform: "'android' | 'ios'",
"version?": "number | string"
})
TypeScriptの型定義とほぼ同じ書き方。新しいDSLを覚える必要がない。学習コストがかなり低い。
3. 優れた型推論
定義したスキーマから自動的にTypeScript型が推論されます。型の二重定義が不要なので、QOL上がります。
4. エラーメッセージが読みやすい
「深くカスタマイズ可能なメッセージと優れたデフォルト設定」があり、バリデーションエラーが明確。デバッグ時のストレスが減る。
5. 集合論ベースの型チェック
extends()メソッドで型の包含関係をランタイムでチェックできます。TypeScriptがコンパイル時にやっていることを、実行時にも使える。これ、動的な型チェックが必要な場面で地味に便利。
6. 内部最適化
すべてのスキーマは内部的に正規化・最適化されます。複数の分岐パスがあるユニオン型でも、最適な判別ロジックが自動生成される。
インストール方法
前提条件
- TypeScript 5.1以上
- package.jsonで
"type": "module"の設定 - tsconfig.jsonで以下の設定:
strictまたはstrictNullChecks(必須)skipLibCheck(強く推奨)exactOptionalPropertyTypes(推奨)
インストール
npm install arktype
pnpm、yarn、bunでもOK。
# pnpm
pnpm install arktype
# yarn
yarn add arktype
# bun
bun install arktype
VSCode設定(推奨)
.vscode/settings.jsonに以下を追加すると、文字列内での自動補完が効くようになります。
{
"editor.quickSuggestions": {
"strings": "on"
},
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^(node:)?os$"
]
}
ArkDark拡張機能をインストールすると、構文ハイライトも強化されます。時短になる。
基本的な使い方
シンプルな型定義
import { type } from 'arktype'
// 基本的なオブジェクト型
const User = type({
name: "string",
email: "string.email",
age: "number"
})
// 型の推論(自動的にTypeScript型として使える)
type User = typeof User.infer
これだけでバリデーター完成。Zodのz.object({ ... })より直感的だと思うんですよね。
バリデーションの実行
const input = {
name: "田中太郎",
email: "tanaka@example.com",
age: 35
}
const result = User(input)
if (result instanceof type.errors) {
// バリデーションエラー
console.error(result.summary)
} else {
// 成功: resultは型安全なUserオブジェクト
console.log(result.name)
}
エラーハンドリングも自然な書き方。
リテラル型とユニオン
const Platform = type("'android' | 'ios' | 'web'")
const Device = type({
platform: "'android' | 'ios'",
version: "string | number"
})
TypeScriptのユニオン型がそのまま使える。
オプショナルプロパティ
const Config = type({
host: "string",
port: "number",
"timeout?": "number", // オプショナル
"retries?": "number"
})
プロパティ名に?をつけるだけ。TypeScriptと同じ。
配列とタプル
// 配列
const Tags = type("string[]")
// タプル
const Coordinate = type(["number", "number"])
// オブジェクトの配列
const Users = type({
name: "string",
age: "number"
}).array()
数値の制約
const Age = type("number > 0 & number < 150")
const Price = type("number >= 0")
const Score = type("integer >= 0 & integer <= 100")
数値の範囲チェックもTypeScript風の構文で書ける。これ、Zodより読みやすいと思います。
文字列のバリデーション
const Email = type("string.email")
const Url = type("string.url")
const Uuid = type("string.uuid")
// 正規表現
const PhoneNumber = type("/^\\d{3}-\\d{4}-\\d{4}$/")
組み込みのバリデーターが充実。
実践的なユースケース
APIレスポンスのバリデーション
import { type } from 'arktype'
const ApiResponse = type({
status: "'success' | 'error'",
data: {
users: {
id: "number",
name: "string",
email: "string.email",
"role?": "'admin' | 'user' | 'guest'"
}.array()
},
"message?": "string"
})
async function fetchUsers() {
const response = await fetch('/api/users')
const json = await response.json()
const result = ApiResponse(json)
if (result instanceof type.errors) {
throw new Error(`Invalid API response: ${result.summary}`)
}
return result.data.users
}
外部APIからのデータは信用できないので、型安全に扱いたい場面で便利。
フォームバリデーション
const ContactForm = type({
name: "1 <= string <= 100",
email: "string.email",
subject: "1 <= string <= 200",
message: "10 <= string <= 5000",
"phone?": "/^\\d{2,4}-\\d{2,4}-\\d{4}$/"
})
function validateForm(formData: unknown) {
const result = ContactForm(formData)
if (result instanceof type.errors) {
// エラーメッセージを整形して返す
return {
valid: false,
errors: result.summary
}
}
return {
valid: true,
data: result
}
}
環境変数のバリデーション
const EnvConfig = type({
NODE_ENV: "'development' | 'production' | 'test'",
PORT: "string.numeric.parse",
DATABASE_URL: "string.url",
"API_KEY?": "string",
"DEBUG?": "'true' | 'false'"
})
function loadConfig() {
const result = EnvConfig(process.env)
if (result instanceof type.errors) {
console.error('環境変数の設定エラー:')
console.error(result.summary)
process.exit(1)
}
return result
}
const config = loadConfig()
アプリ起動時に環境変数をチェック。設定ミスを早期発見できる。
型の再利用と合成
const Address = type({
street: "string",
city: "string",
zipCode: "/^\\d{3}-\\d{4}$/"
})
const Person = type({
name: "string",
age: "number >= 0"
})
// 型の合成
const Employee = type({
...Person.infer,
employeeId: "string",
department: "string",
address: Address
})
extendsによる型チェック
const Animal = type({ name: "string" })
const Dog = type({ name: "string", breed: "string" })
// DogはAnimalを継承しているか?
console.log(Dog.extends(Animal)) // true
// 動的な型チェックに使える
function processAnimal(schema: typeof Animal) {
if (schema.extends(Dog)) {
// Dog固有の処理
}
}
これ、プラグインシステムとか動的な型判定が必要な場面で使える。
Zodからの移行
個人的には、新規プロジェクトではArkType一択だと思います。既存プロジェクトの移行も、構文が似ているので比較的スムーズ。
構文の比較
// Zod
const UserZod = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(0)
})
// ArkType
const UserArk = type({
name: "string",
email: "string.email",
age: "number >= 0"
})
ArkTypeの方がTypeScriptに近くて読みやすい。コード量も減る。
移行のポイント
- まず新機能から: 既存のZodはそのまま、新しいバリデーションをArkTypeで書く
- パフォーマンスが重要な箇所から: ホットパスになる部分を優先的に移行
- 型推論の確認: 移行後も型が正しく推論されているかチェック
まとめ
ArkTypeを導入して感じた変化:
- パフォーマンス: Zodより20倍高速は伊達じゃない
- 学習コスト: TypeScript構文そのままなので、ほぼゼロ
- コード量: 定義が短くなって可読性向上
- 型安全性: コンパイル時とランタイムの型が完全一致
- DX: VSCode拡張で文字列内も補完が効く
正直なところ、「TypeScriptでバリデーション」といえばZodという時代が続いていましたが、ArkTypeはその座を狙える実力があると思います。特にパフォーマンスを気にする場面では。
まだ試していない人は、小さなプロジェクトで使ってみてください。TypeScriptの型をそのまま書く感覚、一度味わうと戻れなくなりますよ。