はじめに
「TypeScriptの型は書いた。バリデーションスキーマも書いた。ついでにOpenAPI用のJSON Schemaも書いた」——気づけば同じデータ構造を3回定義していた、という経験はありませんか?
ランタイムバリデーションといえばZodが定番ですが、ZodのスキーマはZod独自のオブジェクトです。OpenAPIドキュメントの生成やAPIゲートウェイとの連携でJSON Schemaが必要になると、変換レイヤーを挟むことになります。
今回紹介するTypeBoxは、このアプローチを逆転させたライブラリです。スキーマを定義すると、それ自体が標準準拠のJSON Schemaオブジェクトになり、同時にTypeScriptの型としても推論されます。つまり「型」「バリデーション」「JSON Schema」の3つが、たった1つの定義から手に入るのです。
TypeBoxとは
TypeBoxは「JSON Schema Type Builder with Static Type Resolution for TypeScript」を掲げるライブラリです。メモリ上にJSON Schemaオブジェクトを構築し、それをTypeScriptの静的型として推論できるランタイム型システムを提供します。
FastifyやElysiaといった人気フレームワークの型システムの基盤としても採用されており、サーバーサイドTypeScriptの世界では実績十分です。
主な特徴
- JSON Schemaがファーストクラス - 定義したスキーマはそのままJSON Schema(Draft 2020-12対応)として扱えます。OpenAPI生成や他言語との共有に変換不要です
- 静的型推論 -
Type.StaticでスキーマからTypeScriptの型を取り出せます。型とスキーマの二重管理から解放されます - 圧倒的なパフォーマンス - JITコンパイラを備えており、公式ベンチマークではオブジェクト検証で秒間7,800万回と、定番のAjvを上回る速度を記録しています
- 依存関係ゼロ - 軽量で、Node.js・Deno・Bun・ブラウザのどこでも動作します
インストール
TypeBox 1.x はnpmから一発でインストールできます。
# npm
npm install typebox
# pnpm
pnpm add typebox
# bun
bun add typebox
なお、1.x はESM専用・TypeScript 6.0以上が対象です。CommonJS環境や既存プロジェクトでは、Long Term Support版である 0.x 系(パッケージ名は @sinclair/typebox)も引き続き利用できます。
# LTS版(Fastifyなどのエコシステムで広く使われています)
npm install @sinclair/typebox
基本的な使い方
まずはスキーマを定義して、型を取り出してみましょう。
import Type from 'typebox'
// スキーマを定義する
const User = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String({ format: 'email' }),
age: Type.Optional(Type.Number({ minimum: 0 }))
})
// スキーマからTypeScriptの型を推論する
type User = Type.Static<typeof User>
// type User = {
// id: string
// name: string
// email: string
// age?: number
// }
ここで重要なのは、User が特別なオブジェクトではなく素のJSON Schemaだという点です。console.logで覗いてみると、見慣れた形式がそのまま出てきます。
console.log(JSON.stringify(User, null, 2))
// {
// "type": "object",
// "properties": {
// "id": { "type": "string" },
// "name": { "type": "string" },
// ...
// },
// "required": ["id", "name", "email"]
// }
このオブジェクトをそのままOpenAPIの定義に埋め込んだり、ファイルに書き出して他のシステムと共有したりできます。これがZodなど独自スキーマ形式のライブラリとの最大の違いです。
バリデーションを実行する
検証には typebox/schema モジュールを使います。Compile でスキーマをJITコンパイルすると、超高速なバリデータが得られます。
import Type from 'typebox'
import Schema from 'typebox/schema'
const Vector = Schema.Compile(Type.Object({
x: Type.Number(),
y: Type.Number(),
z: Type.Number()
}))
// Check: 真偽値で判定する(型ガードとして機能)
const valid = Vector.Check({ x: 1, y: 0, z: 0 }) // true
// Parse: 検証して型付きの値を返す(不正な値なら例外)
const value = Vector.Parse({ x: 1, y: 0, z: 0 })
// value は { x: number, y: number, z: number } 型
Check は型ガードとして働くため、if 文の中では自動的に型が絞り込まれます。ホットパスで何百万回と呼ばれるAPIサーバーのバリデーションでも、コンパイル済みバリデータなら安心です。
TypeScript構文でスキーマを書く
TypeBox 1.x の目玉機能が Script です。なんと、TypeScriptの型構文をそのまま文字列で書くと、実行時にJSON Schemaへ変換してくれます。
import Type from 'typebox'
const Types = Type.Script(`
type Vector2 = { x: number, y: number }
type Vector3 = { x: number, y: number, z: number }
`)
// Types.Vector3 はJSON Schemaとして利用可能
type Vector3 = Type.Static<typeof Types.Vector3>
Conditional TypesやMapped Typesといった高度な型レベル構文にも対応しており、「TypeScriptの型定義がそのままランタイムスキーマになる」体験は一度味わうと戻れません。
実践的なユースケース
Fastifyで型安全なAPIを作る
TypeBoxが最も輝くのがFastifyとの組み合わせです。LTS版の @sinclair/typebox と型プロバイダを使うと、ルート定義のスキーマからリクエスト・レスポンスの型が自動で推論されます。
npm install fastify @fastify/type-provider-typebox @sinclair/typebox
import Fastify from 'fastify'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import { Type } from '@sinclair/typebox'
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>()
app.post('/users', {
schema: {
body: Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' })
}),
response: {
201: Type.Object({
id: Type.String(),
name: Type.String()
})
}
}
}, async (request, reply) => {
// request.body は { name: string, email: string } と推論済み
const { name } = request.body
reply.code(201)
return { id: crypto.randomUUID(), name }
})
app.listen({ port: 3000 })
スキーマはFastify内部で高速バリデータにコンパイルされ、不正なリクエストは自動で弾かれます。しかも同じスキーマから @fastify/swagger でOpenAPIドキュメントまで生成できるので、「型・検証・APIドキュメント」が完全に一元化されます。
フロントエンドとバックエンドでスキーマを共有する
スキーマが標準JSON Schemaなので、モノレポで共通パッケージに置けばそのまま両側で使えます。
// packages/shared/schemas.ts
import Type from 'typebox'
export const CreatePostInput = Type.Object({
title: Type.String({ minLength: 1, maxLength: 100 }),
body: Type.String({ minLength: 1 }),
tags: Type.Array(Type.String(), { maxItems: 5 })
})
export type CreatePostInput = Type.Static<typeof CreatePostInput>
// フロントエンド側: フォーム送信前のバリデーション
import Schema from 'typebox/schema'
import { CreatePostInput } from '@myapp/shared/schemas'
const validator = Schema.Compile(CreatePostInput)
function handleSubmit(formData: unknown) {
if (validator.Check(formData)) {
// ここでは formData が CreatePostInput 型に絞り込まれている
console.log(`「${formData.title}」を投稿します`)
} else {
console.warn('入力内容に誤りがあります')
}
}
バックエンドでも同じ CreatePostInput をFastifyのスキーマに渡すだけです。バリデーションルールの「フロントとバックで微妙に違う」問題が構造的に発生しなくなります。
Zodとの使い分け
どちらも優れたライブラリなので、プロジェクトの性質で選ぶのがおすすめです。
- TypeBoxが向いているケース - JSON Schema/OpenAPIが絡む、FastifyやElysiaを使っている、バリデーションが性能ボトルネックになり得る大規模API
- Zodが向いているケース -
transformなどの柔軟な変換処理を多用したい、フロントエンド中心でエコシステム(React Hook Formなど)の連携を重視したい
「スキーマの資産を標準形式で持ちたいか」が判断の分かれ目になります。
まとめ
TypeBoxは、TypeScriptの型・ランタイムバリデーション・JSON Schemaという3つの世界を、1つの定義で結びつけてくれるライブラリです。
- スキーマ定義がそのまま標準JSON Schemaになる
Type.Staticで型の二重管理が不要になる- JITコンパイルによりAjvを超える検証速度を発揮する
- Fastifyとの組み合わせで型安全なAPIとドキュメント生成まで一気通貫
とくにOpenAPIを中心に据えたAPI開発では、変換ライブラリを挟まず標準形式で完結する気持ちよさは格別です。まずは手元のAPIのバリデーション1本から、TypeBoxに置き換えてみてはいかがでしょうか。
