はじめに
TypeScriptのランタイムバリデーションは、API のデータ検証やフォーム入力の検証において重要な機能です。昨今ではZodやYupといったライブラリの人気が高いですが、2024年に登場したArkTypeは、その新しいアプローチで注目を集めています。
ArkTypeは「TypeScriptの型定義と1:1で対応するバリデーションライブラリ」として、静的型とランタイム検証の統合を実現しています。
ArkTypeとは?
ArkTypeは、TypeScriptの型構文をそのまま文字列として使用してランタイムバリデーションを行います。従来のライブラリとは異なり、TypeScriptで書く型定義とほぼ同じ構文でバリデーションスキーマを定義できます。
基本的な構文
import { type } from "arktype"
// TypeScriptの型のような記述でバリデーションスキーマを定義
const User = type({
name: "string", // 文字列型
age: "number", // 数値型
platform: "'android' | 'ios'", // リテラル型のユニオン
"email?": "string", // オプショナルプロパティ
tags: "string[]" // 配列型
})
// TypeScriptの型を自動推論
type UserType = typeof User.infer
/*
type UserType = {
name: string;
age: number;
platform: "android" | "ios";
email?: string;
tags: string[];
}
*/
基本的な使い方
// バリデーション対象のデータ
const userData = {
name: "Alice",
age: 28,
platform: "ios",
tags: ["developer", "typescript"]
}
// バリデーション実行
const result = User(userData)
if (result instanceof type.errors) {
// バリデーションエラーの場合
console.error(result.summary)
} else {
// バリデーション成功の場合
console.log("Valid user data:", result)
// resultは型安全なUserTypeとして扱われる
}
他のバリデーションライブラリとの比較
構文の比較
同じバリデーション定義を各ライブラリで比較してみます。
ArkType:
import { type } from "arktype"
const User = type({
name: "string",
email: "email", // 組み込みのemail検証
age: "number>=18",
status: "'active' | 'inactive'"
})
Zod:
import { z } from "zod"
const User = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18),
status: z.enum(["active", "inactive"])
})
Yup:
import * as yup from "yup"
const User = yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
age: yup.number().min(18).required(),
status: yup.string().oneOf(["active", "inactive"]).required()
})
スペック比較表
項目 | ArkType | Zod | Yup |
---|---|---|---|
構文 | TypeScript型文字列 | メソッドチェーン | メソッドチェーン |
パフォーマンス | 非常に高速(Zodの100倍*) | 高速 | 標準的 |
型推論 | 自動的で強力 | 良好 | 基本的 |
エラーメッセージ | 非常に明確 | 良好 | 良好 |
学習コスト | 低(TypeScript知識があれば) | 中程度 | 中程度 |
エコシステム | 新しい | 非常に豊富 | 豊富 |
バンドルサイズ | 小さい | 中程度 | 大きい |
*公式ベンチマーク値
使用例
1. API レスポンスの検証
import { type } from "arktype"
// API レスポンスの型定義
const ApiResponse = type({
success: "boolean",
data: {
id: "number",
title: "string",
description: "string | null",
created_at: "string", // ISO日付文字列
tags: "string[]",
author: {
id: "number",
name: "string",
"avatar?": "string" // オプショナルなアバターURL
}
},
"error?": "string" // エラーメッセージ(エラー時のみ)
})
// APIからのデータを検証
async function fetchPost(id: number) {
try {
const response = await fetch(`/api/posts/${id}`)
const rawData = await response.json()
// ランタイムでデータを検証
const validatedData = ApiResponse(rawData)
if (validatedData instanceof type.errors) {
console.error("API response validation failed:", validatedData.summary)
throw new Error("Invalid API response format")
}
// 型安全なデータとして利用
return validatedData
} catch (error) {
console.error("API fetch error:", error)
throw error
}
}
2. フォームバリデーション
// ユーザー登録フォームの検証
const UserRegistrationForm = type({
username: "string>2", // 3文字以上の文字列
email: "email", // email形式
password: "string>=8", // 8文字以上のパスワード
confirmPassword: "string",
age: "number>=13&<=120", // 13歳以上120歳以下
terms: "true", // 利用規約への同意(trueのみ許可)
"newsletter?": "boolean" // オプショナルなニュースレター購読
})
// フォーム送信時の処理
function handleSubmit(formData: FormData) {
const userData = {
username: formData.get("username"),
email: formData.get("email"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
age: parseInt(formData.get("age") as string),
terms: formData.get("terms") === "on",
newsletter: formData.get("newsletter") === "on"
}
// バリデーション実行
const result = UserRegistrationForm(userData)
if (result instanceof type.errors) {
// エラーを表示
displayValidationErrors(result.by.key) // キー別のエラー情報
return
}
// パスワード一致の追加チェック
if (result.password !== result.confirmPassword) {
displayError("パスワードが一致しません")
return
}
// 登録処理実行
registerUser(result)
}
function displayValidationErrors(errors: Record<string, string[]>) {
Object.entries(errors).forEach(([field, messages]) => {
const errorElement = document.querySelector(`[data-error="${field}"]`)
if (errorElement) {
errorElement.textContent = messages.join(", ")
}
})
}
3. 設定ファイルの検証
// アプリケーション設定の型定義
const AppConfig = type({
app: {
name: "string",
version: "string",
debug: "boolean"
},
database: {
host: "string",
port: "number>0&<=65535", // 有効なポート範囲
username: "string",
password: "string",
"ssl?": "boolean"
},
redis: {
url: "string",
"ttl?": "number>=0" // TTL(秒)
},
logging: {
level: "'debug' | 'info' | 'warn' | 'error'",
"file?": "string"
}
})
// 設定ファイル読み込み時の検証
function loadConfig(configPath: string) {
try {
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'))
const validatedConfig = AppConfig(configData)
if (validatedConfig instanceof type.errors) {
console.error("Configuration validation failed:")
console.error(validatedConfig.summary)
process.exit(1)
}
return validatedConfig
} catch (error) {
console.error("Failed to load configuration:", error)
process.exit(1)
}
}
Pros / Cons
Pros
1. TypeScriptとの完全な互換性
- 既存のTypeScript知識をそのまま活用可能
- 静的型とランタイム検証の乖離がない
2. 優れたパフォーマンス
- 他のライブラリと比較して圧倒的に高速
- 内部最適化により効率的な検証が可能
3. 優れた開発体験
- エディタでの強力な自動補完
- 分かりやすいエラーメッセージ
- ビルドステップ不要
4. 簡潔な記述
- 冗長なメソッドチェーンが不要
- TypeScriptの型定義とほぼ同じ構文
Cons
1. 新しいライブラリ
- エコシステムがまだ発展途上
- 情報やサンプルが限定的
2. 学習コストの存在
- 独特な文字列ベースの構文に慣れが必要
- 複雑な検証ルールの表現方法の習得
3. TypeScript依存
- TypeScriptプロジェクト以外での利用価値が限定的
- JavaScriptオンリーの環境では恩恵が少ない
導入を検討すべきシーン
ArkTypeが適している場合:
- 新規TypeScriptプロジェクト
- パフォーマンスが重要な アプリケーション
- TypeScriptの型定義を多用するプロジェクト
- シンプルで一貫した検証ロジックが欲しい場合
従来のライブラリを選ぶべき場合:
- 既存プロジェクトでZodやYupが深く統合されている
- 豊富なエコシステムや拡張機能が必要
- チームがメソッドチェーン形式に慣れている
まとめ
ArkTypeは、TypeScriptの型システムとランタイム検証を全く新しい方法で統合するライブラリです。その直感的な構文と優れたパフォーマンスは、TypeScriptプロジェクトにおけるバリデーションの実装体験を大幅に向上させる可能性があります。
特に新規プロジェクトや、TypeScriptの型安全性を最大限活用したい場合には、検討する価値が高いライブラリだと思います。ただし、まだ新しいライブラリであるため、プロダクション環境での採用は慎重に検討しましょう。