Superstruct
GitHub概要
ianstormtaylor/superstruct
A simple and composable way to validate data in JavaScript (and TypeScript).
スター7,138
ウォッチ42
フォーク223
作成日:2017年11月23日
言語:TypeScript
ライセンス:MIT License
トピックス
interfacejavascriptschemastructstypestypescriptvalidation
スター履歴
データ取得日時: 2025/10/22 09:49
Superstruct
概要
SuperstructはJavaScript(およびTypeScript)でデータを検証するためのシンプルで構成可能な方法を提供するバリデーションライブラリです。TypeScript、Flow、Go、GraphQLからインスピレーションを受けた型注釈APIにより、馴染みやすく理解しやすいAPIを提供します。特に、アプリケーション固有のカスタムデータ型を定義しやすい設計が特徴で、詳細なランタイムエラー情報を提供することで、デバッグとユーザー体験の向上を図ります。
特徴
- シンプルで構成可能: 小さなビルディングブロックから複雑なスキーマを構築
- カスタムデータ型: アプリケーション固有の型定義を簡単に作成
- 詳細なエラー情報: ランタイムでの詳細なバリデーションエラーを提供
- TypeScript統合: 型推論とコンパイル時型チェックをサポート
- 関数型API:
assert、is、validate、createによる多様な検証方法 - デフォルト値サポート: データ変換と初期値設定の統合
- 軽量設計: 最小限の依存関係でバンドルサイズを削減
- 実用的なエラー: エンドユーザー向けの詳細なエラーメッセージ
インストール
npm install superstruct
# または
yarn add superstruct
# または
pnpm add superstruct
# または
bun add superstruct
使用例
基本的なスキーマ定義
import { assert, object, number, string, array } from 'superstruct'
// 記事スキーマの定義
const Article = object({
id: number(),
title: string(),
tags: array(string()),
author: object({
id: number(),
name: string(),
}),
})
// データの検証
const data = {
id: 34,
title: 'Hello World',
tags: ['news', 'features'],
author: {
id: 1,
name: 'John Doe',
},
}
try {
assert(data, Article)
console.log('データは有効です')
} catch (error) {
console.error('バリデーションエラー:', error.message)
}
TypeScript型推論と型ガード
import { is, object, number, string, Infer } from 'superstruct'
// ユーザースキーマの定義
const User = object({
id: number(),
email: string(),
name: string(),
})
// TypeScript型の推論
type User = Infer<typeof User>
// User = { id: number; email: string; name: string }
// 型ガードとしての使用
function processUser(data: unknown) {
if (is(data, User)) {
// TypeScriptがdataの型を理解している
console.log(`ユーザー: ${data.name} (${data.email})`)
return data.id
}
throw new Error('無効なユーザーデータです')
}
バリデーション結果の取得
import { validate, object, string, number } from 'superstruct'
const PersonSchema = object({
name: string(),
age: number(),
})
const data = { name: 'Alice', age: '30' } // ageが文字列(無効)
// 例外を投げない検証
const [error, result] = validate(data, PersonSchema)
if (error) {
console.error('検証失敗:')
error.failures().forEach(failure => {
console.log(`- ${failure.path.join('.')}: ${failure.message}`)
})
} else {
console.log('検証成功:', result)
}
デフォルト値とデータ変換
import { create, object, string, number, defaulted, optional } from 'superstruct'
// デフォルト値付きスキーマ
const UserProfile = object({
id: defaulted(number(), () => Math.floor(Math.random() * 1000000)),
name: string(),
role: defaulted(string(), 'user'),
email: optional(string()),
createdAt: defaulted(string(), () => new Date().toISOString()),
})
// データの作成と変換
const inputData = {
name: 'Alice',
email: '[email protected]'
}
const user = create(inputData, UserProfile)
console.log(user)
// {
// id: 123456,
// name: 'Alice',
// role: 'user',
// email: '[email protected]',
// createdAt: '2025-07-19T10:30:00.000Z'
// }
カスタムバリデーションの実装
import { define, string, object, refine } from 'superstruct'
// カスタムバリデーター: メールアドレス
const Email = define('Email', (value) => {
return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
})
// カスタムバリデーター: 強力なパスワード
const StrongPassword = define('StrongPassword', (value) => {
if (typeof value !== 'string') return false
return value.length >= 8 &&
/(?=.*[a-z])/.test(value) &&
/(?=.*[A-Z])/.test(value) &&
/(?=.*\d)/.test(value)
})
// 日本の郵便番号バリデーター
const JapaneseZipCode = define('JapaneseZipCode', (value) => {
return typeof value === 'string' && /^\d{3}-\d{4}$/.test(value)
})
// カスタムバリデーターを使用したスキーマ
const UserRegistration = object({
email: Email,
password: StrongPassword,
name: string(),
zipCode: JapaneseZipCode,
})
// refineを使用した条件付きバリデーション
const ProductSchema = refine(object({
type: string(),
name: string(),
price: number(),
weight: optional(number()),
downloadUrl: optional(string()),
}), 'Product', (data) => {
if (data.type === 'physical' && !data.weight) {
return '物理製品には重量が必要です'
}
if (data.type === 'digital' && !data.downloadUrl) {
return 'デジタル製品にはダウンロードURLが必要です'
}
return true
})
複雑なデータ構造の検証
import {
object,
string,
number,
array,
optional,
union,
literal,
record,
tuple
} from 'superstruct'
// 複雑なネストしたオブジェクトスキーマ
const CompanySchema = object({
name: string(),
founded: number(),
status: union([
literal('startup'),
literal('established'),
literal('enterprise')
]),
employees: array(object({
id: number(),
name: string(),
position: string(),
salary: optional(number()),
contacts: object({
email: string(),
phone: optional(string()),
}),
skills: array(string()),
})),
locations: array(object({
country: string(),
city: string(),
address: string(),
coordinates: tuple([number(), number()]), // [緯度, 経度]
})),
metadata: record(string(), union([string(), number(), boolean()])),
})
const companyData = {
name: 'Tech Corp',
founded: 2020,
status: 'established',
employees: [
{
id: 1,
name: 'Alice Johnson',
position: 'Senior Engineer',
salary: 75000,
contacts: {
email: '[email protected]',
phone: '+81-90-1234-5678',
},
skills: ['TypeScript', 'React', 'Node.js'],
},
],
locations: [
{
country: 'Japan',
city: 'Tokyo',
address: '1-1-1 Shibuya, Shibuya-ku',
coordinates: [35.6762, 139.6503],
},
],
metadata: {
industry: 'Technology',
employeeCount: 150,
remote: true,
},
}
assert(companyData, CompanySchema)
API統合での使用例
// Express.jsでの使用例
import express from 'express'
import { assert, object, string, number, optional } from 'superstruct'
const app = express()
app.use(express.json())
// リクエストスキーマの定義
const CreateUserSchema = object({
name: string(),
email: string(),
age: number(),
bio: optional(string()),
})
// バリデーションミドルウェア
const validateBody = (schema: any) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
assert(req.body, schema)
next()
} catch (error: any) {
res.status(400).json({
error: 'バリデーションエラー',
message: error.message,
failures: error.failures?.() || [],
})
}
}
}
app.post('/users', validateBody(CreateUserSchema), (req, res) => {
// req.bodyは検証済み
const user = req.body
res.json({ success: true, user })
})
// レスポンススキーマの定義
const UserResponseSchema = object({
id: number(),
name: string(),
email: string(),
createdAt: string(),
})
app.get('/users/:id', (req, res) => {
const userData = {
id: parseInt(req.params.id),
name: 'John Doe',
email: '[email protected]',
createdAt: new Date().toISOString(),
}
// レスポンスデータの検証
try {
const validatedUser = create(userData, UserResponseSchema)
res.json(validatedUser)
} catch (error: any) {
res.status(500).json({ error: 'データ生成エラー', message: error.message })
}
})
高度なエラーハンドリング
import { StructError, assert, object, string, number } from 'superstruct'
const UserSchema = object({
name: string(),
age: number(),
email: string(),
})
function validateUser(data: unknown) {
try {
assert(data, UserSchema)
return { success: true, data }
} catch (error) {
if (error instanceof StructError) {
const failures = error.failures()
const fieldErrors: Record<string, string[]> = {}
failures.forEach(failure => {
const path = failure.path.join('.')
if (!fieldErrors[path]) {
fieldErrors[path] = []
}
fieldErrors[path].push(failure.message)
})
return {
success: false,
error: {
message: error.message,
fieldErrors,
details: failures.map(f => ({
path: f.path,
message: f.message,
value: f.value,
type: f.type,
})),
},
}
}
return { success: false, error: { message: '予期しないエラーが発生しました' } }
}
}
// 使用例
const result = validateUser({ name: 'Alice', age: 'invalid', email: 'not-email' })
if (!result.success) {
console.log('フィールドエラー:', result.error.fieldErrors)
// {
// age: ['Expected a number, but received: string'],
// email: ['Expected a valid email address']
// }
}
環境設定の検証
import { create, object, string, number, boolean, optional, enums } from 'superstruct'
const EnvSchema = object({
NODE_ENV: enums(['development', 'production', 'test']),
PORT: coerce(number(), string(), (value) => parseInt(value, 10)),
DATABASE_URL: string(),
JWT_SECRET: string(),
REDIS_URL: optional(string()),
DEBUG: coerce(boolean(), string(), (value) => value === 'true'),
LOG_LEVEL: defaulted(enums(['debug', 'info', 'warn', 'error']), 'info'),
})
// coerceヘルパー関数
function coerce<T, U>(target: any, source: any, coercer: (value: U) => T) {
return transform(source, target, coercer)
}
// 環境変数の検証と変換
try {
const config = create(process.env, EnvSchema)
console.log('設定が正常に読み込まれました:', config)
} catch (error: any) {
console.error('環境設定エラー:', error.message)
process.exit(1)
}
フォームバリデーションとの統合
// React Hook Formとの統合例
import { useForm } from 'react-hook-form'
import { assert, object, string, number, boolean } from 'superstruct'
const FormSchema = object({
username: string(),
email: string(),
age: number(),
acceptTerms: boolean(),
})
type FormData = Infer<typeof FormSchema>
// Superstructリゾルバーの作成
const superstructResolver = (schema: any) => {
return (data: any) => {
try {
assert(data, schema)
return { values: data, errors: {} }
} catch (error: any) {
const errors: Record<string, any> = {}
if (error.failures) {
error.failures().forEach((failure: any) => {
const path = failure.path.join('.')
errors[path] = { type: 'validation', message: failure.message }
})
}
return { values: {}, errors }
}
}
}
function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: superstructResolver(FormSchema),
})
const onSubmit = (data: FormData) => {
console.log('フォームデータ:', data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} placeholder="ユーザー名" />
{errors.username && <p>{errors.username.message}</p>}
<input {...register('email')} placeholder="メールアドレス" />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('age', { valueAsNumber: true })} type="number" placeholder="年齢" />
{errors.age && <p>{errors.age.message}</p>}
<label>
<input {...register('acceptTerms')} type="checkbox" />
利用規約に同意する
</label>
{errors.acceptTerms && <p>{errors.acceptTerms.message}</p>}
<button type="submit">登録</button>
</form>
)
}
比較・代替手段
類似ライブラリとの比較
- Zod: より豊富な機能セットと大きなエコシステム、より複雑なAPI
- Yup: オブジェクト指向API、フォームライブラリとの統合が強力
- Joi: Node.js環境に特化、豊富な機能だがブラウザでは重い
- io-ts: 関数型プログラミング指向、学習曲線が急
- Valibot: モジュラー設計でより軽量、新しいライブラリ
Superstructを選ぶべき場合
- シンプルで直感的なAPIを重視する
- カスタムバリデーションロジックを多用する
- 軽量なバンドルサイズが重要
- 詳細なエラー情報が必要
- 関数型プログラミングスタイルを好む
- TypeScriptとJavaScriptの両方で使用したい
他のライブラリを選ぶべき場合
- より豊富な組み込みバリデーターが必要(Zod、Yup)
- フォーム専用の機能が必要(Yup)
- 最新のモジュラー設計を求める(Valibot)
- 非常に複雑な型変換が必要(io-ts)
学習リソース
まとめ
Superstructは、シンプルさと構成可能性を重視したバリデーションライブラリです。特に、アプリケーション固有のカスタムデータ型を定義しやすく、詳細なランタイムエラー情報を提供することで、開発者とエンドユーザーの両方に優れた体験を提供します。軽量でありながら強力なカスタマイズ性を持ち、TypeScriptプロジェクトでもJavaScriptプロジェクトでも効果的に活用できる柔軟性が最大の特徴です。特に、APIバリデーション、設定ファイルの検証、ユーザー入力の検証など、データの整合性が重要なあらゆる場面で威力を発揮します。