superstruct の使い方 — JavaScriptとTypeScriptのためのデータバリデーションライブラリ
一言でいうと
superstruct は、シンプルで合成可能(composable)なAPIを通じて、JavaScriptおよびTypeScriptのデータ構造をバリデーションするライブラリです。スキーマ定義がそのままTypeScriptの型として機能するため、ランタイムバリデーションと型安全性を同時に実現できます。
どんな時に使う?
- APIのリクエスト/レスポンスのバリデーション — 外部から受け取るJSONデータが期待する構造を持っているか検証したいとき
- フォーム入力値の検証 — ユーザー入力がスキーマに合致するかをランタイムでチェックし、型安全に扱いたいとき
- 設定ファイルや環境変数のパース — アプリケーション起動時に設定値が正しい型・形式であることを保証したいとき
インストール
# npm
npm install superstruct
# yarn
yarn add superstruct
# pnpm
pnpm add superstruct
以下の解説は superstruct v2.x 系を前提としています。v1.x 以前とはAPIに差異がある場合があります。
基本的な使い方
最も基本的なパターンは、object でスキーマを定義し、assert または is でバリデーションを行うことです。
import { object, string, number, assert, is, validate } from 'superstruct';
// スキーマ定義
const User = object({
name: string(),
age: number(),
email: string(),
});
// --- パターン1: assert(不正なら例外をスロー) ---
const input: unknown = { name: '田中太郎', age: 30, email: 'tanaka@example.com' };
assert(input, User);
// ここを通過すれば input は { name: string; age: number; email: string } として型が絞り込まれる
console.log(input.name); // '田中太郎'
// --- パターン2: is(真偽値を返す) ---
if (is(input, User)) {
console.log(input.email); // 型安全にアクセス可能
}
// --- パターン3: validate(エラー情報を取得) ---
const [error, value] = validate(input, User);
if (error) {
console.error(error.message);
} else {
console.log(value); // { name: '田中太郎', age: 30, email: 'tanaka@example.com' }
}
よく使うAPI
1. プリミティブ型スキーマ
import { string, number, boolean, date, any, unknown as unknownStruct } from 'superstruct';
const Name = string(); // string
const Age = number(); // number
const Active = boolean(); // boolean
const CreatedAt = date(); // Date オブジェクト
2. object — オブジェクトスキーマの定義
import { object, string, number, optional } from 'superstruct';
const Profile = object({
username: string(),
bio: optional(string()), // undefined を許容
followers: number(),
});
// TypeScriptの型を抽出
import type { Infer } from 'superstruct';
type ProfileType = Infer<typeof Profile>;
// => { username: string; bio?: string; followers: number }
3. array / tuple — 配列・タプル
import { array, string, number, tuple } from 'superstruct';
// string の配列
const Tags = array(string());
// => string[]
// 固定長タプル
const Coordinate = tuple([number(), number()]);
// => [number, number]
assert(['TypeScript', 'Node.js'], Tags); // OK
assert([35.6762, 139.6503], Coordinate); // OK
4. union / literal / enums — ユニオン型・リテラル型・列挙型
import { union, literal, enums, string, number } from 'superstruct';
// ユニオン型
const StringOrNumber = union([string(), number()]);
// リテラル型
const Direction = union([
literal('north'),
literal('south'),
literal('east'),
literal('west'),
]);
// 列挙型(文字列の集合)
const Role = enums(['admin', 'editor', 'viewer']);
assert('admin', Role); // OK
assert('guest', Role); // StructError がスローされる
5. coerce / defaulted — 値の変換とデフォルト値
import { coerce, string, number, defaulted, object, create, date } from 'superstruct';
// 文字列を Date に変換するスキーマ
const MyDate = coerce(date(), string(), (value) => new Date(value));
const result = create('2024-01-15', MyDate);
console.log(result); // Date オブジェクト: 2024-01-15T00:00:00.000Z
// デフォルト値付きスキーマ
const Config = object({
host: defaulted(string(), 'localhost'),
port: defaulted(number(), 3000),
});
const config = create({}, Config);
console.log(config); // { host: 'localhost', port: 3000 }
注意:
createはassertと異なり、coerceやdefaultedによる変換を適用した値を返します。バリデーションだけでなく変換も行いたい場合はcreateを使ってください。
6. refine — カスタムバリデーション
import { refine, string, number, assert } from 'superstruct';
// メールアドレスの簡易バリデーション
const Email = refine(string(), 'Email', (value) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
});
// 正の整数
const PositiveInt = refine(number(), 'PositiveInt', (value) => {
return Number.isInteger(value) && value > 0;
});
assert('user@example.com', Email); // OK
assert('invalid-email', Email); // StructError
assert(42, PositiveInt); // OK
assert(-1, PositiveInt); // StructError
類似パッケージとの比較
| 特徴 | superstruct | zod | yup | joi |
|---|---|---|---|---|
| バンドルサイズ | 非常に小さい(~4KB gzip) | 中程度(~13KB gzip) | 中程度(~15KB gzip) | 大きい(~30KB+) |
| TypeScript推論 | ◎ Infer で自動推論 | ◎ ネイティブ対応 | △ 部分的 | × 別途型定義が必要 |
| API設計思想 | 関数合成ベース | メソッドチェーン | メソッドチェーン | メソッドチェーン |
| coerce(型変換) | ○ | ○ | ○ | ○ |
| エラーメッセージのカスタマイズ | △ シンプル | ◎ 柔軟 | ◎ 柔軟 | ◎ 柔軟 |
| ブラウザ/Node.js | 両対応 | 両対応 | 両対応 | 主にNode.js |
| 学習コスト | 低い | 低い | 中程度 | 中程度 |
選定の目安:
- バンドルサイズを最小限にしたい → superstruct
- エコシステムの充実度・エラーメッセージの柔軟性を重視 → zod
- React Hook Form との統合が主目的 → zod または yup
- サーバーサイドのみで使う → joi も選択肢に入る
注意点・Tips
1. assert vs create の使い分け
assert はバリデーションのみを行い、値を返しません(void)。coerce や defaulted を使ったスキーマでは create を使わないと変換が適用されません。
// ❌ assert では coerce が適用されない
assert('2024-01-15', MyDate); // バリデーションは通るが Date に変換されない
// ✅ create なら変換後の値が返る
const d = create('2024-01-15', MyDate); // Date オブジェクトが返る
2. Infer 型を活用する
スキーマから型を二重定義しないようにしましょう。Infer を使えばスキーマ定義が Single Source of Truth になります。
import type { Infer } from 'superstruct';
const Article = object({
title: string(),
body: string(),
publishedAt: optional(date()),
});
// 手動で型を書かない
type Article = Infer<typeof Article>;
3. StructError のハンドリング
assert がスローする StructError には、どのフィールドがどう不正だったかの詳細情報が含まれます。
import { assert, object, string, number, StructError } from 'superstruct';
const User = object({ name: string(), age: number() });
try {
assert({ name: 123, age: 'not a number' }, User);
} catch (e) {
if (e instanceof StructError) {
console.log(e.key); // 'name'(最初に失敗したキー)
console.log(e.type); // 'string'
console.log(e.path); // ['name']
console.log(e.value); // 123
// すべてのエラーを取得
for (const failure of e.failures()) {
console.log(failure.key, failure.message);
}
}
}
4. ネストしたオブジェクトの再利用
スキーマは通常のJavaScript変数なので、自由に合成できます。
const Address = object({
street: string(),
city: string(),
zip: string(),
});
const Company = object({
name: string(),
address: Address, // 再利用
});
const Employee = object({
name: string(),
company: Company, // さらにネスト
});
5. partial でオプショナルに変換
既存のスキーマの全フィールドをオプショナルにしたい場合(PATCH APIなど)に便利です。
import { partial, object, string, number, create } from 'superstruct';
const User = object({
name: string(),
age: number(),
});
const PartialUser = partial(User);
// => { name?: string; age?: number }
const patched = create({ name: '更新太郎' }, PartialUser); // age がなくてもOK
まとめ
superstruct は、関数合成ベースのシンプルなAPIと極めて小さなバンドルサイズが魅力のバリデーションライブラリです。Infer による型推論でスキーマと型定義を一元管理でき、TypeScriptプロジェクトとの相性が非常に優れています。バンドルサイズに敏感なフロントエンドプロジェクトや、シンプルで見通しの良いバリデーションを求めるケースで特に力を発揮するでしょう。