Superstruct

TypeScriptJavaScriptValidationSchemaRuntime ValidationComposableCustom Types

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

スター履歴

ianstormtaylor/superstruct Star History
データ取得日時: 2025/10/22 09:49

Superstruct

概要

SuperstructはJavaScript(およびTypeScript)でデータを検証するためのシンプルで構成可能な方法を提供するバリデーションライブラリです。TypeScript、Flow、Go、GraphQLからインスピレーションを受けた型注釈APIにより、馴染みやすく理解しやすいAPIを提供します。特に、アプリケーション固有のカスタムデータ型を定義しやすい設計が特徴で、詳細なランタイムエラー情報を提供することで、デバッグとユーザー体験の向上を図ります。

特徴

  • シンプルで構成可能: 小さなビルディングブロックから複雑なスキーマを構築
  • カスタムデータ型: アプリケーション固有の型定義を簡単に作成
  • 詳細なエラー情報: ランタイムでの詳細なバリデーションエラーを提供
  • TypeScript統合: 型推論とコンパイル時型チェックをサポート
  • 関数型API: assertisvalidatecreateによる多様な検証方法
  • デフォルト値サポート: データ変換と初期値設定の統合
  • 軽量設計: 最小限の依存関係でバンドルサイズを削減
  • 実用的なエラー: エンドユーザー向けの詳細なエラーメッセージ

インストール

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バリデーション、設定ファイルの検証、ユーザー入力の検証など、データの整合性が重要なあらゆる場面で威力を発揮します。