12k
All articles

ArkType:Zodより高速な代替ライブラリ

ArkTypeとZodの構文、型推論、検証速度、APIレスポンス処理を比較。ArkTypeを選ぶ場面と、Zodを使い続ける場面がわかります。

OpenReplay Team
OpenReplay Team
ArkType:Zodより高速な代替ライブラリ

ArkTypeは、TypeScript風の文字列でスキーマを記述し、それを最適化されたバリデーターにコンパイルするTypeScript向けランタイムバリデーターです。安定したZodコードベースを持つチームにとって、全面移行に見合う価値があるケースはまれです。一方、新規プロジェクトやバリデーションがホットパスになっている箇所であれば、試してみる価値があります。

Zodを使っている方なら、ArkTypeのベンチマークグラフを目にして、新しい構文を覚えてまで得る価値のある速度なのか気になったことがあるでしょう。

本記事では、1つのUserスキーマをZodからArkTypeへ移行します。型推論、パフォーマンス、APIレスポンスのバリデーション、トレードオフを取り上げたうえで、Zodを使い続けるべきケースを率直に説明します。サンプルコードはArkType 2.2とZod 4を使用しています。

重要なポイント

  • ArkTypeはスキーマをTypeScript風の文字列で定義します。そのため、Zodではz.enum(["android", "ios"])と書くところを、ArkTypeの"'android' | 'ios'"は生成されるユニオン型そのままに読めます。
  • ArkTypeの型を未知のデータに対して呼び出すと、検証済みの値かArkErrorsインスタンスのいずれかが返されます。そのため、慣用的なチェック方法はout instanceof type.errorsです。
  • ArkTypeのホームページでは、ランタイムでZod 4より20倍高速だと謳っています。ただしこれはベンダー自身による数値であり、その後Zod 4.5ではホットパス向けにz.compile()が追加されています。
  • ArkType 2.2ではtype()内で任意のStandard Schemaバリデーターを受け付けるため、既存のZod 4スキーマを書き直すことなく、ArkTypeの定義内にネストできます。

ArkTypeとZodの違いをひと言で表すと?

Zodはメソッドチェーンでスキーマを組み立てますが、ArkTypeはスキーマをTypeScript風の文字列で記述するため、生成される型そのもののように読めます。"(number | string)[]"と宣言されたフィールドは、いずれにせよ書くことになるTypeScriptの型注釈を引用符で囲んだものにすぎません。ArkTypeの「Your First Type」ガイドが指摘しているとおり、エディターはTypeScript自身の型システムを使って、入力中にこれらの文字列定義をチェックし、補完も提供します。

同じUserスキーマをZodとArkTypeで書く

以下は、両ライブラリで書いた同じ3フィールドのスキーマです。必須の文字列、2値のユニオン、そして数値または文字列からなるオプショナルな配列で構成されています。

Zod 4:

import * as z from "zod"

const User = z.object({
  name: z.string(),
  platform: z.enum(["android", "ios"]),
  versions: z.array(z.union([z.number(), z.string()])).optional(),
})

ArkType 2.2:

import { type } from "arktype"

const User = type({
  name: "string",
  platform: "'android' | 'ios'",
  "versions?": "(number | string)[]",
})

ArkType版にはネストしたビルダー呼び出しがありません。オプショナルの指定位置も異なります。ArkTypeはTypeScriptと同様にキーに?を付けますが、Zodは値に対して.optional()を呼び出します。また、ArkTypeは同名のTypeScriptコンパイラーオプションに対応するグローバルなexactOptionalPropertyTypes設定(2.1.12で追加)もサポートしています。

比較項目Zod 4ArkType 2.2
スキーマ構文ビルダーメソッドのチェーンTypeScript風の文字列とオブジェクトリテラル
オプショナルフィールド値に対して.optional()キーに"key?"
静的型z.infer<typeof User>typeof User.infer
未知のデータの検証User.safeParse(data)User(data)
失敗の判定!result.successout instanceof type.errors
読みやすいエラーメッセージresult.error.issuesから組み立てるout.summary
コンパイルz.compile()によるオプトイン(Zod 4.5以降)定義の処理方式に組み込み済み

型推論:typeof User.infer と z.infer

どちらのライブラリもランタイムスキーマから静的型を導出するため、インターフェースを手書きする必要はありません。違うのは型を取り出す構文だけです。

// Zod
type User = z.infer<typeof User>

// ArkType
type User = typeof User.infer

Zodのz.inferは、スキーマの型に適用するジェネリックなユーティリティです。ArkTypeのinferは型自体のプロパティであり、typeofを通じて参照します。上記のスキーマでは、どちらも{ name: string; platform: "android" | "ios"; versions?: (number | string)[] }という同じ形の型になります。

ArkTypeはZodより高速なのか?

ArkTypeは、各Typeの生成時に最適化されたバリデーターを事前に構築します。ArkTypeの設定ドキュメントでは、この事前コンパイルの仕組みと、それを無効化するjitlessオプションについて説明されています。ArkTypeのホームページでは、ランタイムでZod 4より20倍高速だと謳っています。ただしこれはベンダー自身のベンチマークによる主張であり、第三者による計測ではありません。

その後、Zod 4.5ではz.compile()によってこの差の一部が縮まりました。Zodのコンパイルに関するドキュメントによると、z.compile()はスキーマを一度だけ走査し、分岐のない直線的なチェック関数を生成して、Zodがそれをnew Function()で実行します。入力がこの高速チェックに失敗した場合は通常のパーサーに処理を引き渡すため、詳細なエラー内容は変わりません。ZodのREADMEでは、55種類のスキーマを使ったベンチマークで中央値2.4倍の高速化が報告されています。したがって、コンパイルしていないZod 4との比較からは、コンパイル済みのZodスキーマに対してArkTypeがどの程度の性能を発揮するかはわかりません。

フォームのバリデーションでは、ArkTypeとZodの速度差が問題になることはほとんどありません。ユーザー操作ごとに数回のバリデーションが走る程度では、ボトルネックにはならないからです。速度が問題になるのは、リクエストハンドラーやバッチインポートのように、1つのスキーマが毎秒数千回実行される場合です。

APIレスポンスのエンドツーエンドなバリデーション

実務で最もよくあるのは、アプリの他の部分がデータを信頼する前にfetchのレスポンスをチェックする処理です。以下は両ライブラリで書いた同じ関数です。

ArkType:

async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  const out = User(await res.json())

  if (out instanceof type.errors) {
    console.error(out.summary)
    return null
  }
  return out // typed as User
}

Zod:

async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  const result = User.safeParse(await res.json())

  if (!result.success) {
    console.error(result.error.issues)
    return null
  }
  return result.data
}

ZodとArkTypeでは、バリデーションの戻り値の形が異なります。ZodのsafeParseは、success、data、errorを持つ判別可能な結果オブジェクトを返します。ArkTypeの型は、検証済みの値そのものかArkErrorsインスタンスのいずれかを返します。instanceofチェックの後、TypeScriptはoutをUser型に絞り込みます。out.summaryは、失敗したすべてのパスについて、期待された内容と実際に受け取った内容を列挙した、読みやすい1つのメッセージです。

また、ArkErrorsはJSON.stringify()にそのまま渡すこともできます。この変更はArkType 2.1.10で導入され、2.2のリリースノートにも記載されています。つまり、フォーマッターを書かなくても、バリデーションエラーをそのままAPIのエラーレスポンスやログエントリーに出力できます。

トレードオフ:エコシステムと習熟度 vs 新しい文法

ArkTypeに対するZodの最大の強みは、その周辺環境です。Zodには大規模な連携ライブラリのエコシステムがあり、ほとんどのTypeScript開発者はすでにZodのスキーマを読めます。この差の一部は、Zod、ArkType、Valibotがいずれも実装している共通インターフェースStandard Schemaによって縮まりつつあります。乗り換える前に、使用しているフォームライブラリ、ルーター、RPCレイヤーがStandard Schemaに対応しているか確認しましょう。

ArkTypeには現実的なコストもあります。

  • 新しい文法を覚える必要がある。 ユニオン、配列、オプショナルキーは見慣れた書き方ですが、制約やより高度な式は、チームが習得しなければならない文字列ベースの言語です。
  • ミスは文字列に対する型エラーとして表れる。 "strng"のようなタイプミスはエディター上でTypeScriptが検出しますが、存在しないメソッドのエラーではなく文字列定義に対するエラーとして表示されるため、チームは新しい種類のエラーメッセージの読み方に慣れる必要があります。

ArkType 2.2では、部分的な導入が可能です。ArkTypeのインテグレーションに関するドキュメントに示されているとおり、type()は単体でもオブジェクト定義の内部でも任意のStandard Schemaバリデーターを受け付け、ネイティブのArkType定義と同様に型推論とチェックを行います。つまり、既存のZod 4スキーマはそのまま残せます。

import * as z from "zod"
import { type } from "arktype"

const ZodDevice = z.object({ platform: z.enum(["android", "ios"]) })

const User = type({
  name: "string",
  device: ZodDevice, // Standard Schema validator nested in ArkType (2.1.28+)
})

速度よりもバンドルサイズが主な制約であれば、Valibotを検討するとよいでしょう。

ZodからArkTypeに乗り換えるべきタイミングは?

安定したZodコードベースと正常に動作する連携機能を持つチームの多くは、現時点ではArkTypeに乗り換えるべきではありません。移行コストは、Zod 4.5のコンパイラーですでに一部カバーされている速度向上を上回ります。ArkTypeを試すべきなのは、次のような場合です。

  • 新規プロジェクトを始める場合で、使用するツール群がStandard Schemaに対応している。
  • 実証済みのホットパスがある場合。高スループットのエンドポイントやバッチジョブなどで、プロファイリングによってバリデーションのコストが確認されている。
  • 1つのエンドポイントで試したい場合。既存のZodスキーマを書き直さずに、ArkTypeの定義内にネストして利用する。

それ以外の場合は、引き続きZodでデータを検証し、最も頻繁に実行されるスキーマでz.compile()を試してみましょう。

まとめ

ArkTypeの最大の強みは可読性です。スキーマが生成される型そのもののように見え、しかも高速なコンパイル済みバリデーターが得られます。一方、Zodの強みは、すでに技術スタックに組み込まれていることです。低コストで違いを検証するには、バリデーション負荷の高いエンドポイントを1つ選び、既存のZodスキーマをネストしつつArkType 2.2で定義したうえで、他の部分に手を加える前に、同じZodスキーマをz.compile()でコンパイルしたバージョンと比較してプロファイリングしてみてください。

よくある質問

ArkTypeはCloudflare Workersや厳格なContent Security Policy下でも動作しますか?

はい。ArkTypeはTypeのインスタンス化時にnew Functionを使ってバリデーションロジックを事前コンパイルしますが、Cloudflare Workersのようにnew Functionをサポートしない環境では、この機能を自動的に無効化します。'unsafe-eval'を許可しないCSP下では、jitlessオプションをtrueに設定してください。組み込みキーワードにも設定が反映されるよう、'arktype'から何かをインポートする前に、'arktype/config'から設定を行います。事前コンパイルされたバリデーターは使われなくなりますが、バリデーション自体は引き続き機能します。

ArkTypeはZod 4のようにJSON Schemaを生成できますか?

はい。すべてのArkType TypeにはtoJsonSchema()メソッドがあり、さらにArkType 2.2では逆方向の変換、つまりJSON SchemaをArkType Typeに変換する@ark/json-schemaパッケージが追加されました。morph、シンボルキー、DateなどJSON Schemaに相当する表現がない機能を含む場合、toJsonSchema()はデフォルトで例外をスローしますが、fallbackオプションを使えば各ケースを個別に処理できます。Zod 4ではz.toJSONSchema()が同様の役割を担います。

Zodのtransform()に相当するArkTypeの機能は何ですか?

ArkTypeでは変換処理をmorphと呼び、.pipe()でアタッチします。例:type('string').pipe(s => s.trim())。'string.json.parse'や'string.numeric.parse'といった組み込みのparseキーワードを使えば、コールバックなしで一般的な変換を行えます。morphが例外をスローした場合、ArkTypeは意図的にクラッシュさせたものとみなします。スローされた例外をArkErrorsの結果に変換したい場合は、代わりに.pipe.try()を使用してください。推論される型にはmorphの出力が反映されます。

ArkTypeで、エラーを返す代わりにZodのparse()のように不正なデータに対して例外をスローできますか?

はい。ArkErrorsの結果に対してout.throw()を呼び出すと例外がスローされます。また、グローバルなonFailオプションを使えば、すべてのTypeが不正なデータに対して例外をスローするようになります。'arktype/config'からconfigure({ onFail: errors => errors.throw() })のように設定してください。呼び出し結果がArkErrorsを返さなくなることをTypeScriptに認識させるため、グローバルなArkEnvインターフェースにも同じonFailを宣言します。デフォルトの戻り値スタイルはZodのsafeParse()に、onFailはparse()に相当します。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.