Agent Skills by ALSEL
Anthropic ClaudeLLM・AI開発⭐ リポ 0品質スコア 50/100

zod

フォームバリデーション・APIバリデーション・ランタイム型チェックに対応した、TypeScriptファーストのスキーマバリデーションライブラリで、コンパイル時の型と静的型推論を統合して活用できます。

description の原文を見る

TypeScript-first schema validation library with static type inference for form validation, API validation, and runtime type checking with compile-time types.

SKILL.md 本文

Zod 検証スキル

サマリー

TypeScript-first スキーマ検証ライブラリで、静的型推論を備えています。スキーマを一度定義すれば、実行時検証とコンパイル時型が自動的に得られます。

いつ使うか

  • 型安全なデータを伴うフォーム検証
  • API リクエスト/レスポンス検証
  • 環境変数検証
  • TypeScript 推論を伴うランタイム型チェック
  • tRPC プロシージャのインプット/アウトプット
  • データベーススキーマ検証 (Drizzle, Prisma)

クイックスタート

import { z } from 'zod';

// スキーマを定義
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().min(18),
  role: z.enum(['user', 'admin'])
});

// TypeScript型を推論
type User = z.infer<typeof UserSchema>;

// データを検証
const result = UserSchema.safeParse(data);
if (result.success) {
  const user: User = result.data;
}
<!-- SECTION: primitives -->

プリミティブ型

基本型

import { z } from 'zod';

// 検証付き文字列
const nameSchema = z.string()
  .min(2, "短すぎます")
  .max(50, "長すぎます")
  .trim();

const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{3}$/);

// 数値
const ageSchema = z.number()
  .int("整数である必要があります")
  .positive()
  .min(0)
  .max(120);

const priceSchema = z.number()
  .positive()
  .multipleOf(0.01); // 通貨の精度

// 真偽値
const isActiveSchema = z.boolean();

// 日付
const createdAtSchema = z.date()
  .min(new Date('2020-01-01'))
  .max(new Date());

const dateStringSchema = z.string().datetime(); // ISO 8601
const dateOnlySchema = z.string().date(); // YYYY-MM-DD

特殊型

// リテラル値
const roleSchema = z.literal('admin');
const statusSchema = z.literal('pending');

// 列挙型
const ColorEnum = z.enum(['red', 'green', 'blue']);
type Color = z.infer<typeof ColorEnum>; // 'red' | 'green' | 'blue'

const NativeEnum = z.nativeEnum(MyEnum); // TypeScript enum用

// Nullable と Optional
const optionalString = z.string().optional(); // string | undefined
const nullableString = z.string().nullable(); // string | null
const nullishString = z.string().nullish(); // string | null | undefined

// デフォルト値
const countSchema = z.number().default(0);
const settingsSchema = z.object({
  theme: z.string().default('light'),
  notifications: z.boolean().default(true)
});
<!-- SECTION: objects_and_arrays -->

オブジェクトと配列

オブジェクトスキーマ

// 基本的なオブジェクト
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string(),
  age: z.number().optional()
});

// ネストされたオブジェクト
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
  zipCode: z.string()
});

const PersonSchema = z.object({
  name: z.string(),
  address: AddressSchema,
  contacts: z.object({
    email: z.string().email(),
    phone: z.string().optional()
  })
});

// Strict vs Passthrough
const strictSchema = z.object({ name: z.string() }).strict();
// 未知のキーを拒否

const passthroughSchema = z.object({ name: z.string() }).passthrough();
// 未知のキーを許可

const stripSchema = z.object({ name: z.string() }).strip();
// 未知のキーを削除(デフォルト)

配列スキーマ

// シンプルな配列
const stringArray = z.array(z.string());
const numberArray = z.array(z.number()).min(1).max(10);

// オブジェクトの配列
const UsersSchema = z.array(UserSchema);

// 空でない配列
const tagSchema = z.array(z.string()).nonempty("最低1つのタグが必要");

// 固定長配列(タプル)
const coordinateSchema = z.tuple([z.number(), z.number()]);
type Coordinate = z.infer<typeof coordinateSchema>; // [number, number]

// Rest付きタプル
const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());
// [string, number, ...string[]]

レコードとマップ

// レコード(動的キーを持つオブジェクト)
const userRolesSchema = z.record(
  z.string(), // キー型
  z.enum(['admin', 'user', 'guest']) // 値型
);
type UserRoles = z.infer<typeof userRolesSchema>;
// { [key: string]: 'admin' | 'user' | 'guest' }

// マップ
const configMapSchema = z.map(
  z.string(), // キー
  z.number()  // 値
);

// セット
const uniqueTagsSchema = z.set(z.string());
<!-- SECTION: type_inference -->

型推論

import { z } from 'zod';

// 出力型を推論
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  age: z.number()
});

type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number }

// 入力型を推論(変換前)
const TransformSchema = z.object({
  date: z.string().transform(s => new Date(s))
});

type Input = z.input<typeof TransformSchema>;
// { date: string }

type Output = z.output<typeof TransformSchema>;
// { date: Date }

// 推論された型を関数で使う
function createUser(data: User): void {
  // dataは型安全
}

function validateAndCreate(data: unknown): User | null {
  const result = UserSchema.safeParse(data);
  return result.success ? result.data : null;
}
<!-- SECTION: validation_methods -->

検証メソッド

Parse vs SafeParse

// parse() - 失敗時に例外をスロー
try {
  const user = UserSchema.parse(data);
  // userは型User
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error(error.issues);
  }
}

// safeParse() - 結果オブジェクトを返す
const result = UserSchema.safeParse(data);

if (result.success) {
  const user = result.data; // 型User
} else {
  const errors = result.error.issues;
  errors.forEach(err => {
    console.log(`${err.path}: ${err.message}`);
  });
}

// parseAsync() - 非同期リファイン用
const asyncResult = await UserSchema.parseAsync(data);

// safeParseAsync() - 安全な非同期版
const asyncSafeResult = await UserSchema.safeParseAsync(data);

部分的な検証

// スキーマに合致するかチェック(例外をスローしない)
const isValid = UserSchema.safeParse(data).success;

// カスタム型ガード
function isUser(data: unknown): data is User {
  return UserSchema.safeParse(data).success;
}

if (isUser(unknownData)) {
  // TypeScriptはunknownDataがUserであることを知っている
  console.log(unknownData.email);
}
<!-- SECTION: schema_composition -->

スキーマの構成

拡張とマージ

// 拡張(フィールドを追加)
const BaseUserSchema = z.object({
  id: z.string(),
  email: z.string()
});

const AdminUserSchema = BaseUserSchema.extend({
  role: z.literal('admin'),
  permissions: z.array(z.string())
});

// マージ(スキーマを結合)
const NameSchema = z.object({ name: z.string() });
const AgeSchema = z.object({ age: z.number() });

const PersonSchema = NameSchema.merge(AgeSchema);
// { name: string; age: number }

// ピック(フィールドを選択)
const UserIdEmail = UserSchema.pick({ id: true, email: true });

// オミット(フィールドを除外)
const UserWithoutId = UserSchema.omit({ id: true });

// Partial(全フィールドをオプション化)
const PartialUser = UserSchema.partial();

// DeepPartial(再帰的にpartial化)
const DeepPartialUser = UserSchema.deepPartial();

// Required(全フィールドを必須化)
const RequiredUser = UserSchema.required();

Union と Intersection

// Union (OR)
const StringOrNumber = z.union([z.string(), z.number()]);
// 短縮形
const StringOrNumberAlt = z.string().or(z.number());

// Discriminated Union (タグ付きunion)
const SuccessResponse = z.object({
  status: z.literal('success'),
  data: z.any()
});

const ErrorResponse = z.object({
  status: z.literal('error'),
  message: z.string()
});

const ApiResponse = z.discriminatedUnion('status', [
  SuccessResponse,
  ErrorResponse
]);

// Intersection (AND)
const User = z.object({ name: z.string() });
const Timestamps = z.object({
  createdAt: z.date(),
  updatedAt: z.date()
});

const UserWithTimestamps = z.intersection(User, Timestamps);
// 短縮形
const UserWithTimestampsAlt = User.and(Timestamps);
<!-- SECTION: transformations -->

変換とリファイン

Transform

// 検証後にデータを変換
const StringToNumber = z.string().transform(val => parseInt(val, 10));

const DateSchema = z.string().transform(str => new Date(str));

// 変換をチェーン
const TrimmedLowercase = z.string()
  .transform(s => s.trim())
  .transform(s => s.toLowerCase());

// 検証付きの変換
const PositiveStringNumber = z.string()
  .transform(val => parseInt(val, 10))
  .refine(n => n > 0, "正の数である必要があります");

// 複雑な変換
const UserInputSchema = z.object({
  name: z.string().transform(s => s.trim()),
  email: z.string().email().transform(s => s.toLowerCase()),
  birthDate: z.string().transform(s => new Date(s)),
  tags: z.string().transform(s => s.split(',').map(t => t.trim()))
});

type UserInput = z.input<typeof UserInputSchema>;
// { name: string; email: string; birthDate: string; tags: string }

type User = z.output<typeof UserInputSchema>;
// { name: string; email: string; birthDate: Date; tags: string[] }

Refine (カスタム検証)

// シンプルなリファイン
const PasswordSchema = z.string()
  .min(8)
  .refine(
    val => /[A-Z]/.test(val),
    "大文字を含む必要があります"
  )
  .refine(
    val => /[0-9]/.test(val),
    "数字を含む必要があります"
  );

// カスタムエラー付きリファイン
const UniqueEmailSchema = z.string().email().refine(
  async (email) => {
    const exists = await checkEmailExists(email);
    return !exists;
  },
  { message: "メールアドレスは既に使用されています" }
);

// オブジェクトレベルのリファイン
const PasswordMatchSchema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).refine(
  data => data.password === data.confirmPassword,
  {
    message: "パスワードが一致しません",
    path: ["confirmPassword"] // エラー位置
  }
);

// 複数フィールド検証
const DateRangeSchema = z.object({
  startDate: z.date(),
  endDate: z.date()
}).refine(
  data => data.endDate > data.startDate,
  {
    message: "終了日は開始日より後である必要があります",
    path: ["endDate"]
  }
);

SuperRefine (高度な検証)

// 複雑な検証のためのZodコンテキストへのアクセス
const ComplexSchema = z.object({
  type: z.enum(['email', 'phone']),
  value: z.string()
}).superRefine((data, ctx) => {
  if (data.type === 'email') {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(data.value)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "無効なメールアドレス形式",
        path: ["value"]
      });
    }
  } else if (data.type === 'phone') {
    const phoneRegex = /^\+?[1-9]\d{1,14}$/;
    if (!phoneRegex.test(data.value)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "無効な電話番号形式",
        path: ["value"]
      });
    }
  }
});

// 複数のissue
const RegistrationSchema = z.object({
  username: z.string(),
  email: z.string(),
  age: z.number()
}).superRefine(async (data, ctx) => {
  // ユーザー名の可用性をチェック
  if (await usernameTaken(data.username)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "ユーザー名は既に使用されています",
      path: ["username"]
    });
  }

  // メールアドレスの可用性をチェック
  if (await emailTaken(data.email)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "メールアドレスは既に登録されています",
      path: ["email"]
    });
  }

  // 年齢制限
  if (data.age < 18) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "18歳以上である必要があります",
      path: ["age"]
    });
  }
});
<!-- SECTION: error_handling -->

エラーハンドリング

カスタムエラーメッセージ

// フィールドレベルのメッセージ
const UserSchema = z.object({
  email: z.string().email({ message: "無効なメールアドレス" }),
  age: z.number({
    required_error: "年齢は必須です",
    invalid_type_error: "年齢は数値である必要があります"
  }).min(18, { message: "18歳以上である必要があります" })
});

// グローバルエラーマップ
import { z } from 'zod';

const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    if (issue.expected === "string") {
      return { message: "このフィールドはテキストである必要があります" };
    }
  }
  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === "string") {
      return { message: `最低${issue.minimum}文字必要です` };
    }
  }
  return { message: ctx.defaultError };
};

z.setErrorMap(customErrorMap);

エラー処理

// フォーム用にエラーをフラット化
const result = UserSchema.safeParse(data);

if (!result.success) {
  const flatErrors = result.error.flatten();

  console.log(flatErrors.formErrors); // トップレベルのエラー
  console.log(flatErrors.fieldErrors);
  // { email: ["無効なメール"], age: ["18歳以上である必要があります"] }
}

// API レスポンス用にフォーマット
function formatZodError(error: z.ZodError) {
  return error.issues.map(issue => ({
    field: issue.path.join('.'),
    message: issue.message
  }));
}

// 使用例
const result = UserSchema.safeParse(data);
if (!result.success) {
  return res.status(400).json({
    errors: formatZodError(result.error)
  });
}
<!-- SECTION: async_validation -->

非同期検証

import { z } from 'zod';

// 非同期リファイン
const UsernameSchema = z.string().refine(
  async (username) => {
    const available = await checkUsernameAvailable(username);
    return available;
  },
  { message: "ユーザー名は既に使用されています" }
);

// parseAsync または safeParseAsync を使用する必要があります
const result = await UsernameSchema.safeParseAsync("john_doe");

// 複雑な非同期検証
const RegistrationSchema = z.object({
  username: z.string().refine(
    async (val) => !(await usernameTaken(val)),
    "ユーザー名は既に使用されています"
  ),
  email: z.string().email().refine(
    async (val) => !(await emailTaken(val)),
    "メールアドレスは既に登録されています"
  ),
  inviteCode: z.string().refine(
    async (code) => await validateInviteCode(code),
    "無効なインビットコード"
  )
});

// 検証
const userData = await RegistrationSchema.parseAsync(input);

// エラーハンドリング付き
const result = await RegistrationSchema.safeParseAsync(input);
if (!result.success) {
  // 検証エラーを処理
}
<!-- SECTION: advanced_types -->

高度な型

再帰的な型

// 自己参照スキーマ
type Category = {
  name: string;
  subcategories: Category[];
};

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema)
  })
);

// ツリー構造
type TreeNode = {
  value: number;
  left?: TreeNode;
  right?: TreeNode;
};

const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
  z.object({
    value: z.number(),
    left: TreeNodeSchema.optional(),
    right: TreeNodeSchema.optional()
  })
);

Discriminated Union

// discriminatorフィールドに基づいた型安全な union
const Circle = z.object({
  kind: z.literal('circle'),
  radius: z.number()
});

const Rectangle = z.object({
  kind: z.literal('rectangle'),
  width: z.number(),
  height: z.number()
});

const Triangle = z.object({
  kind: z.literal('triangle'),
  base: z.number(),
  height: z.number()
});

const Shape = z.discriminatedUnion('kind', [
  Circle,
  Rectangle,
  Triangle
]);

type Shape = z.infer<typeof Shape>;

// TypeScript は discriminator に基づいて型を絞り込める
function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}

Preprocess

// 検証前にデータを変換
const NumberFromString = z.preprocess(
  (val) => (typeof val === 'string' ? parseInt(val, 10) : val),
  z.number()
);

// 検証前にデータをクリーン
const TrimmedString = z.preprocess(
  (val) => (typeof val === 'string' ? val.trim() : val),
  z.string()
);

// JSON文字列をパース
const JsonSchema = z.preprocess(
  (val) => (typeof val === 'string' ? JSON.parse(val) : val),
  z.object({
    name: z.string(),
    age: z.number()
  })
);

// フォームデータの前処理
const FormDataSchema = z.preprocess(
  (data) => {
    // FormDataをオブジェクトに変換
    if (data instanceof FormData) {
      return Object.fromEntries(data.entries());
    }
    return data;
  },
  z.object({
    name: z.string(),
    email: z.string().email()
  })
);

Branded Types

// 名義的な型を作成
const UserId = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof UserId>;

const Email = z.string().email().brand<'Email'>();
type Email = z.infer<typeof Email>;

// 似た型の混同を防ぐ
function getUserById(id: UserId) { /* ... */ }
function sendEmail(to: Email) { /* ... */ }

const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const email = Email.parse('user@example.com');

getUserById(userId); // ✓
getUserById(email);  // ✗ 型エラー
<!-- SECTION: integrations -->

インテグレーション

React Hook Form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const FormSchema = z.object({
  username: z.string().min(3, "最低3文字"),
  email: z.string().email("無効なメール"),
  age: z.number().min(18, "18歳以上である必要があります")
});

type FormData = z.infer<typeof FormSchema>;

function MyForm() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>({
    resolver: zodResolver(FormSchema)
  });

  const onSubmit = (data: FormData) => {
    // dataは検証され型安全
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username')} />
      {errors.username && <span>{errors.username.message}</span>}

      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age && <span>{errors.age.message}</span>}

      <button type="submit">送信</button>
    </form>
  );
}

tRPC

import { z } from 'zod';
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

const router = t.router;
const publicProcedure = t.procedure;

// インプット/アウトプット検証
const appRouter = router({
  userById: publicProcedure
    .input(z.object({
      id: z.string().uuid()
    }))
    .output(z.object({
      id: z.string().uuid(),
      name: z.string(),
      email: z.string().email()
    }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({
        where: { id: input.id }
      });
      return user; // アウトプットスキーマに対して型チェック
    }),

  createUser: publicProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
      age: z.number().min(18)
    }))
    .mutation(async ({ input }) => {
      return await db.user.create({ data: input });
    })
});

export type AppRouter = typeof appRouter;

Next.js API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().min(18).optional()
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validatedData = CreateUserSchema.parse(body);

    // validatedDataは検証され型付き
    const user = await createUser(validatedData);

    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { errors: error.flatten().fieldErrors },
        { status: 400 }
      );
    }
    return NextResponse.json(
      { error: '内部サーバーエラー' },
      { status: 500 }
    );
  }
}

// クエリパラメータの検証
const SearchParamsSchema = z.object({
  page: z.string().transform(Number).pipe(z.number().min(1)).default('1'),
  limit: z.string().transform(Number).pipe(z.number().max(100)).default('10'),
  sort: z.enum(['asc', 'desc']).default('asc')
});

export async function GET(request: NextRequest) {
  const searchParams = Object.fromEntries(
    request.nextUrl.searchParams.entries()
  );

  const params = SearchParamsSchema.parse(searchParams);
  // params は { page: number, limit: number, sort: 'asc' | 'desc' }

  const users = await getUsers(params);
  return NextResponse.json(users);
}

Express ミドルウェア

import express from 'express';
import { z } from 'zod';

// 検証ミドルウェア
const validate = (schema: z.ZodSchema) => {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    try {
      schema.parse(req.body);
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          errors: error.flatten().fieldErrors
        });
      }
      next(error);
    }
  };
};

const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(18)
});

app.post('/users', validate(CreateUserSchema), async (req, res) => {
  // req.bodyは検証済み(Express では型付きではない)
  const user = await createUser(req.body);
  res.json(user);
});

// params, query, body を検証
const validateRequest = (schema: {
  params?: z.ZodSchema;
  query?: z.ZodSchema;
  body?: z.ZodSchema;
}) => {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    try {
      if (schema.params) {
        req.params = schema.params.parse(req.params);
      }
      if (schema.query) {
        req.query = schema.query.parse(req.query);
      }
      if (schema.body) {
        req.body = schema.body.parse(req.body);
      }
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({ errors: error.issues });
      }
      next(error);
    }
  };
};

app.get(
  '/users/:id',
  validateRequest({
    params: z.object({ id: z.string().uuid() }),
    query: z.object({ include: z.string().optional() })
  }),
  async (req, res) => {
    // 検証済みの params と query
  }
);

Drizzle ORM

import { z } from 'zod';
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';

// テーブルを定義
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  age: integer('age')
});

// スキーマを自動生成
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);

// 検証をカスタマイズ
export const customInsertUserSchema = createInsertSchema(users, {
  email: z.string().email(),
  age: z.number().min(18).optional()
});

// アプリケーションで使う
type NewUser = z.infer<typeof insertUserSchema>;
type User = z.infer<typeof selectUserSchema>;

function createUser(data: unknown) {
  const validatedData = insertUserSchema.parse(data);
  return db.insert(users).values(validatedData);
}

環境変数

// env.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(32),
  PORT: z.string().transform(Number).pipe(z.number().min(1024)),
  REDIS_HOST: z.string().default('localhost'),
  REDIS_PORT: z.string().transform(Number).default('6379'),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});

// 起動時に検証
export const env = envSchema.parse(process.env);

// 型安全な環境変数
export type Env = z.infer<typeof envSchema>;

// 使用例
console.log(`Server running on port ${env.PORT}`);
// env.PORT は string ではなく number
<!-- SECTION: best_practices -->

ベストプラクティス

スキーマの整理

// schemas/user.schema.ts
import { z } from 'zod';

// 再利用可能なプリミティブ
export const emailSchema = z.string().email();
export const uuidSchema = z.string().uuid();
export const passwordSchema = z.string()
  .min(8)
  .regex(/[A-Z]/, "大文字を含む必要があります")
  .regex(/[0-9]/, "数字を含む必要があります");

// 基本スキーマ
export const baseUserSchema = z.object({
  id: uuidSchema,
  email: emailSchema,
  name: z.string().min(2)
});

// 拡張スキーマ
export const createUserSchema = baseUserSchema.omit({ id: true }).extend({
  password: passwordSchema,
  confirmPassword: z.string()
}).refine(
  data => data.password === data.confirmPassword,
  { message: "パスワードが一致しません", path: ["confirmPassword"] }
);

export const updateUserSchema = baseUserSchema.partial().omit({ id: true });

// 型をエクスポート
export type User = z.infer<typeof baseUserSchema>;
export type CreateUser = z.infer<typeof createUserSchema>;
export type UpdateUser = z.infer<typeof updateUserSchema>;

パフォーマンス最適化

// パースされたスキーマをキャッシュ
const userSchemaCache = new Map<string, z.ZodSchema>();

function getCachedSchema(key: string, factory: () => z.ZodSchema) {
  if (!userSchemaCache.has(key)) {
    userSchemaCache.set(key, factory());
  }
  return userSchemaCache.get(key)!;
}

// 大きなオブジェクト用の遅延検証
const lazyUserSchema = z.lazy(() => z.object({
  // アクセス時にのみ検証
  profile: complexProfileSchema,
  settings: complexSettingsSchema
}));

// 配列のストリーミング検証
async function validateLargeArray(items: unknown[]) {
  const errors: z.ZodError[] = [];

  for (const item of items) {
    const result = ItemSchema.safeParse(item);
    if (!result.success) {
      errors.push(result.error);
    }
  }

  return errors;
}

スキーマのテスト

import { describe, it, expect } from 'vitest';

describe('UserSchema', () => {
  it('正しいユーザーデータを検証する', () => {
    const validUser = {
      email: 'user@example.com',
      name: 'John Doe',
      age: 25
    };

    expect(() => UserSchema.parse(validUser)).not.toThrow();
  });

  it('無効なメールアドレスを拒否する', () => {
    const invalidUser = {
      email: 'not-an-email',
      name: 'John',
      age: 25
    };

    const result = UserSchema.safeParse(invalidUser);
    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.issues[0].path).toEqual(['email']);
    }
  });

  it('変換を正しく適用する', () => {
    const input = {
      name: '  JOHN DOE  ',
      email: 'USER@EXAMPLE.COM'
    };

    const result = UserSchema.parse(input);
    expect(result.name).toBe('john doe');
    expect(result.email).toBe('user@example.com');
  });
});

一般的なパターン

// 条件付き検証
const ConditionalSchema = z.object({
  type: z.enum(['personal', 'business']),
  data: z.any()
}).transform((val) => {
  if (val.type === 'personal') {
    return {
      type: val.type,
      data: PersonalDataSchema.parse(val.data)
    };
  } else {
    return {
      type: val.type,
      data: BusinessDataSchema.parse(val.data)
    };
  }
});

// ページネーションスキーマ
export const paginationSchema = z.object({
  page: z.number().min(1).default(1),
  limit: z.number().min(1).max(100).default(20),
  sort: z.string().optional(),
  order: z.enum(['asc', 'desc']).default('asc')
});

// フィルタースキーマ
export const filterSchema = z.object({
  search: z.string().optional(),
  status: z.enum(['active', 'inactive', 'pending']).optional(),
  dateFrom: z.string().datetime().optional(),
  dateTo: z.string().datetime().optional()
});

// API レスポンスラッパー
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
  z.object({
    success: z.boolean(),
    data: dataSchema.optional(),
    error: z.string().optional(),
    timestamp: z.string().datetime()
  });

const userResponseSchema = apiResponseSchema(UserSchema);

Yup/Joi からのマイグレーション

// Yup -> Zod
// Yup
const yupSchema = yup.object({
  email: yup.string().email().required(),
  age: yup.number().min(18).required()
});

// Zod相当
const zodSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18)
});

// Joi -> Zod
// Joi
const joiSchema = Joi.object({
  email: Joi.string().email().required(),
  age: Joi.number().min(18).required()
});

// Zod相当(上と同じ)
const zodSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18)
});

// 主な違い:
// 1. Zod のフィールドはデフォルトで必須
// 2. Zod には一流の TypeScript 統合がある
// 3. Zod スキーマはイミュータブル
// 4. Zod はより良いツリーシェイキングを実現

その他のリソース

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
bobmatnyc
リポジトリ
bobmatnyc/claude-mpm-skills
ライセンス
MIT
最終更新
不明

Source: https://github.com/bobmatnyc/claude-mpm-skills / ライセンス: MIT

関連スキル

OpenAILLM・AI開発⭐ リポ 6,054

agent-browser

AI エージェント向けのブラウザ自動化 CLI です。ウェブサイトとの対話が必要な場合に使用します。ページ遷移、フォーム入力、ボタンクリック、スクリーンショット取得、データ抽出、ウェブアプリのテスト、ブラウザ操作の自動化など、あらゆるブラウザタスクに対応できます。「ウェブサイトを開く」「フォームに記入する」「ボタンをクリックする」「スクリーンショットを取得する」「ページからデータを抽出する」「このウェブアプリをテストする」「サイトにログインする」「ブラウザ操作を自動化する」といった要求や、プログラマティックなウェブ操作が必要なタスクで起動します。

by JimmyLv
汎用LLM・AI開発⭐ リポ 1,982

anyskill

AnySkill — あなたのプライベート・スキルクラウド。GitHubを基盤としたリポジトリからエージェントスキルを管理、同期、動的にロードできます。自然言語でクラウドスキルを検索し、オンデマンドでプロンプトを自動ロード、カスタムスキルのアップロードと共有、スキルバンドルの一括インストールが可能です。OpenClaw、Antigravity、Claude Code、Cursorに対応しています。

by LeoYeAI
汎用LLM・AI開発⭐ リポ 1,982

engram

AIエージェント向けの永続的なメモリシステムです。バグ修正、意思決定、発見、設定変更の後はmem_saveを使用してください。ユーザーが「覚えている」「記憶している」と言及した場合、または以前のセッションと重複する作業を開始する際はmem_searchを使用します。セッション終了前にmem_session_summaryを使用して、コンテキストを保持してください。

by LeoYeAI
汎用LLM・AI開発⭐ リポ 21,584

skyvern

AI駆動のブラウザ自動化により、任意のウェブサイトを自動化できます。フォーム入力、データ抽出、ファイルダウンロード、ログイン、複数ステップのワークフロー実行など、ユーザーがウェブサイトと連携する必要があるときに使用します。Skyvernは、LLMとコンピュータビジョンを活用して、未知のサイトも自動操作可能です。Python SDK、TypeScript SDK、REST API、MCPサーバー、またはCLIを通じて統合できます。

by Skyvern-AI
汎用LLM・AI開発⭐ リポ 1,149

pinchbench

PinchBenchベンチマークを実行して、OpenClawエージェントの実世界タスクにおけるパフォーマンスを評価できます。モデルの機能テスト、モデル間の比較、ベンチマーク結果のリーダーボード提出、またはOpenClawのセットアップがカレンダー、メール、リサーチ、コーディング、複数ステップのワークフローにどの程度対応しているかを確認する際に使用します。

by pinchbench
汎用LLM・AI開発⭐ リポ 4,693

openui

OpenUIとOpenUI Langを使用してジェネレーティブUIアプリを構築できます。これらはLLM生成インターフェースのためのトークン効率的なオープン標準です。OpenUI、@openuidev、ジェネレーティブUI、LLMからのストリーミングUI、AI向けコンポーネントライブラリ、またはjson-render/A2UIの置き換えについて述べる際に使用します。スキャフォルディング、defineComponent、システムプロンプト、Renderer、およびOpenUI Lang出力のデバッグに対応しています。

by thesysdev
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: bobmatnyc · bobmatnyc/claude-mpm-skills · ライセンス: MIT