はじめに
TypeScriptで開発していると、「型定義を書いたのに、なんでバリデーション用に同じような定義をもう一回書かなきゃいけないんだ」と思うこと、ありませんか。
Zodを使えばある程度解決するんですけど、それでも独自のAPIを覚える必要がある。で、最近見つけたのがArkType。これ、TypeScriptの型構文をそのまま使ってランタイム検証ができるんですよ。
GitHubで7,400スター以上、月間150万回以上のnpmダウンロード。正直なところ、最初は「またバリデーションライブラリか」と思ったんですが、使ってみたら考え方が変わりました。
ArkTypeとは
ArkTypeは「TypeScript's 1:1 validator」を謳う、ランタイム検証ライブラリです。
特徴的なのは、TypeScriptの型構文をそのまま文字列として書けるということ。z.string()みたいなメソッドチェーンではなく、"string"という文字列リテラルで型を定義できる。
6年以上の開発歴があり、53人以上のコントリビューターが参加。MITライセンスで公開されています。最新コミットは毎日のように行われていて、かなりアクティブに開発されている印象。
特徴・メリット
1. 圧倒的なパフォーマンス
これ、公式が出している数字なんですけど、オブジェクトの検証が14ナノ秒。Zodの20倍速い、Yupの2,000倍速いとのこと。
個人的には、そこまでパフォーマンスがクリティカルなケースは少ないかもしれないですが、大量のデータを処理するバッチとか、リアルタイム性が求められるアプリでは効いてくる話。
2. TypeScriptの型構文がそのまま使える
import { type } from "arktype"
// これがArkTypeの基本形
const user = type({
name: "string",
age: "number",
email: "string.email"
})
見覚えある構文ですよね。TypeScriptを書いてる人なら、新しいAPIを覚えるコストがほぼゼロ。
3. エディタ統合が優秀
型定義を書いている最中に、オートコンプリートが効く。しかもプラグインやビルドステップなしで。これ、地味に開発体験がいいんですよ。
4. 集合論ベースの型理解
ArkTypeは内部的に集合論を使って型の関係を理解している。Union型やIntersection型の判定が賢いらしく、自動的に最適化された判定ロジックを生成してくれる。
5. エラーメッセージがわかりやすい
デフォルトのエラーメッセージがかなり親切。カスタマイズも柔軟にできるので、ユーザー向けのエラー表示にそのまま使えるレベル。
インストール方法
前提条件
TypeScriptプロジェクトがセットアップされていること。TypeScript 5.1以上を推奨。
インストール
npm install arktype
これだけ。追加の型定義パッケージとかは不要です。
バンドルサイズ
約47.6kB(minified + gzipped)。Zodと比べると大きめですが、高速なパフォーマンスとトレードオフといったところ。
基本的な使い方
基本の型定義
import { type } from "arktype"
// プリミティブ型
const stringType = type("string")
const numberType = type("number")
const booleanType = type("boolean")
// オブジェクト型
const userType = type({
id: "number",
name: "string",
email: "string.email",
age: "number >= 0"
})
// 検証
const result = userType({
id: 1,
name: "田中太郎",
email: "tanaka@example.com",
age: 30
})
if (result instanceof type.errors) {
console.error(result.summary)
} else {
// resultは型安全
console.log(result.name) // 田中太郎
}
TypeScriptを書いてきた人なら、すんなり読める構文だと思います。
オプショナルとデフォルト値
const configType = type({
host: "string",
port: "number = 3000", // デフォルト値
"debug?": "boolean", // オプショナル
"timeout?": "number = 5000" // オプショナル + デフォルト値
})
オプショナルは?サフィックス、デフォルト値は=で指定。直感的ですね。
Union型とLiteral型
// Union型
const statusType = type("'pending' | 'active' | 'completed'")
// 数値のUnion
const diceType = type("1 | 2 | 3 | 4 | 5 | 6")
// 複合Union
const responseType = type({
status: "'success' | 'error'",
data: "unknown"
})
TypeScriptのUnion型と同じ書き方。
配列とタプル
// 配列
const numbersType = type("number[]")
const stringsType = type("string[]")
// オブジェクトの配列
const usersType = type({
id: "number",
name: "string"
}).array()
// タプル
const coordinateType = type(["number", "number"])
const namedTupleType = type(["string", "number", "boolean"])
数値の制約
// 範囲指定
const ageType = type("number >= 0 & number <= 150")
// 整数
const intType = type("number % 1") // 1で割り切れる = 整数
// 組み合わせ
const scoreType = type("0 <= number <= 100")
この範囲指定の書き方、かなり直感的で好きですね。
文字列の制約
const emailType = type("string.email")
const urlType = type("string.url")
const uuidType = type("string.uuid")
// 正規表現
const phoneType = type("/^0\\d{9,10}$/")
// 長さ制約
const usernameType = type("3 <= string <= 20")
組み込みのバリデーターが充実していて、だいたいのケースはカバーできる。
実践的なユースケース
APIレスポンスの検証
import { type } from "arktype"
const apiResponseType = type({
success: "boolean",
data: {
users: {
id: "number",
name: "string",
email: "string.email",
"createdAt?": "string"
}[]
},
"error?": {
code: "string",
message: "string"
}
})
async function fetchUsers() {
const response = await fetch("/api/users")
const json = await response.json()
const result = apiResponseType(json)
if (result instanceof type.errors) {
throw new Error(`Invalid API response: ${result.summary}`)
}
return result.data.users
}
外部APIからのレスポンスは信用できないので、こういう検証は必須。ArkTypeなら型安全に扱える。
フォームバリデーション
const signupFormType = type({
username: "3 <= string <= 20",
email: "string.email",
password: "8 <= string <= 100",
confirmPassword: "string",
"acceptTerms": "true" // リテラル型でtrueのみ許可
})
function validateSignupForm(formData: unknown) {
const result = signupFormType(formData)
if (result instanceof type.errors) {
// エラーをフィールドごとに整理
const fieldErrors: Record<string, string> = {}
for (const error of result) {
const path = error.path.join(".")
fieldErrors[path] = error.message
}
return { success: false, errors: fieldErrors }
}
// パスワード一致チェック(これはArkType外で)
if (result.password !== result.confirmPassword) {
return {
success: false,
errors: { confirmPassword: "パスワードが一致しません" }
}
}
return { success: true, data: result }
}
環境変数の検証
const envType = type({
NODE_ENV: "'development' | 'staging' | 'production'",
DATABASE_URL: "string",
PORT: "string", // 環境変数は文字列で来る
"API_KEY?": "string",
"DEBUG?": "'true' | 'false'"
})
function loadEnv() {
const result = envType(process.env)
if (result instanceof type.errors) {
console.error("環境変数の設定エラー:")
console.error(result.summary)
process.exit(1)
}
return {
nodeEnv: result.NODE_ENV,
databaseUrl: result.DATABASE_URL,
port: parseInt(result.PORT, 10),
apiKey: result.API_KEY,
debug: result.DEBUG === "true"
}
}
export const env = loadEnv()
起動時に環境変数を検証しておくと、後から「あの変数がundefinedだった」みたいなバグを防げる。
型の再利用と合成
// 基本型を定義
const addressType = type({
postalCode: "/^\\d{3}-\\d{4}$/",
prefecture: "string",
city: "string",
street: "string"
})
const contactType = type({
email: "string.email",
"phone?": "/^0\\d{9,10}$/"
})
// 合成
const customerType = type({
id: "number",
name: "string",
address: addressType,
contact: contactType
})
// 静的型の取得
type Customer = typeof customerType.infer
typeof xxx.inferで静的な型を取り出せる。Zodのz.infer<typeof xxx>と同じ発想ですね。
Zodとの比較
正直なところ、ZodとArkTypeはどちらも優秀なライブラリです。選ぶ基準を整理すると:
ArkTypeを選ぶ理由
- TypeScriptの型構文に慣れていて、新しいAPIを覚えたくない
- パフォーマンスが重要なユースケースがある
- 範囲指定など、直感的な記法が好み
Zodを選ぶ理由
- エコシステムが成熟している(tRPC、React Hook Formとの統合など)
- ドキュメントや情報量が多い
- バンドルサイズを小さくしたい(Zodは約14kB)
個人的には、新規プロジェクトでパフォーマンスを重視するならArkType、既存のエコシステムとの統合を重視するならZod、という使い分けかなと思います。
まとめ
ArkTypeを使ってみて感じたこと:
- 学習コスト: TypeScriptの型を知っていればほぼゼロ
- パフォーマンス: Zodの20倍速いは伊達じゃない
- 開発体験: エディタ補完が効いて快適
- エラーメッセージ: デフォルトで十分わかりやすい
- 型推論:
typeof xxx.inferで静的型も取れる
30代になって思うのは、新しいライブラリを試すのに躊躇する必要はないということ。特にArkTypeは、TypeScriptを書いてきた人なら「あ、これ知ってる」という感覚で使い始められる。
バリデーションライブラリに不満がある人、パフォーマンスを改善したい人は、一度試してみる価値はあると思います。