ブログ記事

TypeScript型ガード実践ガイド2025 - Zodから最新ライブラリまで

手動の型ガードはもう古い?Zod(38.6k★)、Valibot(7.7k★)、Effect(9.4k★)など最新ライブラリを使った型安全な実装方法を徹底解説。バンドルサイズとパフォーマンス比較付き。

更新:
プログラミング
TypeScript 型安全性 Zod バリデーション パターンマッチング
TypeScript型ガード実践ガイド2025 - Zodから最新ライブラリまでのヒーロー画像

Typescript の型ガードは強力ですが、手動実装は煩雑でエラーの温床になりがち。2025 年現在、優れたライブラリを活用することで、より安全で保守しやすいコードが書けるようになりました。

この記事で学べること

  • 基本的な型ガードから最新のライブラリ活用まで
  • Zod、Valibot、Effect、ts-pattern の実践的な使い方
  • 各ライブラリのパフォーマンス比較と選定基準
  • 実際のプロジェクトでの導入事例

なぜライブラリを使うべきか?

手動の型ガード実装には以下の問題があります:

  1. 実装の重複 - 似たような型ガードを何度も書く
  2. 保守性の低下 - 型定義と実装が分離して不整合が生じやすい
  3. 実行時エラー - 型ガードの実装ミスによるバグ
  4. スケーラビリティ - 複雑な型に対応しづらい

1. Zod - デファクトスタンダードのスキーマバリデーション

基本的な使い方

interface User {
  id: string;
  name: string;
  age: number;
  email: string;
}

function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    typeof data.id === 'string' &&
    'name' in data &&
    typeof data.name === 'string' &&
    'age' in data &&
    typeof data.age === 'number' &&
    'email' in data &&
    typeof data.email === 'string'
  );
}
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  age: z.number().int().min(0).max(150),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

// バリデーション
const result = UserSchema.safeParse(data);
if (result.success) {
  // result.data は User型
  console.log(result.data);
}
手動の型ガード
interface User {
  id: string;
  name: string;
  age: number;
  email: string;
}

function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    typeof data.id === 'string' &&
    'name' in data &&
    typeof data.name === 'string' &&
    'age' in data &&
    typeof data.age === 'number' &&
    'email' in data &&
    typeof data.email === 'string'
  );
}
Zodを使った実装
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  age: z.number().int().min(0).max(150),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

// バリデーション
const result = UserSchema.safeParse(data);
if (result.success) {
  // result.data は User型
  console.log(result.data);
}

高度な機能

// APIレスポンスの型安全な処理
const ApiResponseSchema = z.discriminatedUnion('status', [
  z.object({
    status: z.literal('success'),
    data: z.object({
      users: z.array(UserSchema),
      total: z.number(),
    }),
  }),
  z.object({
    status: z.literal('error'),
    error: z.object({
      code: z.string(),
      message: z.string(),
    }),
  }),
]);

// 型の変換も可能
const DateSchema = z.string().transform((str) => new Date(str));

// カスタムバリデーション
const PasswordSchema = z.string()
  .min(8, 'パスワードは8文字以上必要です')
  .regex(/[A-Z]/, '大文字を含む必要があります')
  .regex(/[0-9]/, '数字を含む必要があります');

2. Valibot - 軽量高速な次世代バリデーター

Valibot は、Zod より軽量(約 95%小さい、最小 700 バイト)で最速のバリデーションライブラリです。2025 年現在 v1.1.0 がリリースされ、プロダクション利用が増えています。

import * as v from 'valibot';

// スキーマ定義
const UserSchema = v.object({
  id: v.string(),
  name: v.string(),
  age: v.pipe(
    v.number(),
    v.integer(),
    v.minValue(0),
    v.maxValue(150)
  ),
  email: v.pipe(v.string(), v.email()),
});

// 使用方法
try {
  const user = v.parse(UserSchema, data);
  // userは型安全
} catch (error) {
  if (v.isValiError(error)) {
    console.log(error.issues);
  }
}

// 非同期バリデーション
const AsyncUserSchema = v.objectAsync({
  email: v.pipeAsync(
    v.string(),
    v.email(),
    v.customAsync(async (email) => {
      const exists = await checkEmailExists(email);
      return !exists || 'このメールアドレスは既に使用されています';
    })
  ),
});
主要バリデーションライブラリの比較(2025年6月時点)
ライブラリ GitHubスター バンドルサイズ パフォーマンス 特徴
Zod 38.6k 12.8kb 標準 豊富な機能、大規模エコシステム
Valibot 7.7k 0.7kb~ 最速 モジュラー設計、Tree-shaking完全対応
Effect 9.4k 25kb~ 高速 包括的な型システム、FP指向
Yup 22.8k 15.2kb やや遅い 歴史が長い、Formikとの相性良
io-ts 6.7k 8.5kb 高速 関数型プログラミング指向

3. Effect - 関数型エラーハンドリング

Effect は、より高度な型安全性とエラーハンドリングを提供します。

import { Effect, Schema } from '@effect/schema';

// スキーマ定義
const User = Schema.struct({
  id: Schema.string,
  name: Schema.string,
  age: Schema.number.pipe(
    Schema.int(),
    Schema.between(0, 150)
  ),
  email: Schema.string.pipe(Schema.pattern(/^.+@.+$/)),
});

// Effectを使った処理
const fetchUser = (id: string) =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`).then(r => r.json()),
    catch: () => new Error('Network error'),
  }).pipe(
    Effect.flatMap((data) => Schema.parse(User)(data)),
    Effect.catchTag('ParseError', (error) =>
      Effect.fail(new Error('Invalid user data'))
    )
  );

// 実行
Effect.runPromise(fetchUser('123'))
  .then(user => console.log(user))
  .catch(error => console.error(error));

4. ts-pattern - 強力なパターンマッチング

ts-pattern は、型ガードを超えたパターンマッチングを提供します。

import { match, P } from 'ts-pattern';

// Union型の処理
type Shape = 
  | { type: 'circle'; radius: number }
  | { type: 'rectangle'; width: number; height: number }
  | { type: 'triangle'; base: number; height: number };

const area = (shape: Shape) =>
  match(shape)
    .with({ type: 'circle' }, ({ radius }) => Math.PI * radius ** 2)
    .with({ type: 'rectangle' }, ({ width, height }) => width * height)
    .with({ type: 'triangle' }, ({ base, height }) => (base * height) / 2)
    .exhaustive();

// APIレスポンスの処理
const handleResponse = (response: unknown) =>
  match(response)
    .with({ status: P.number.between(200, 299) }, () => 'Success')
    .with({ status: 400 }, () => 'Bad Request')
    .with({ status: 401 }, () => 'Unauthorized')
    .with({ status: P.number.between(500, 599) }, () => 'Server Error')
    .otherwise(() => 'Unknown Error');

// 複雑な条件分岐
const processUser = (user: unknown) =>
  match(user)
    .with(
      {
        age: P.number.gte(18),
        email: P.string.includes('@'),
        role: P.union('admin', 'user'),
      },
      (validUser) => {
        // validUserは型が絞り込まれている
        return `Valid ${validUser.role}`;
      }
    )
    .with({ age: P.number.lt(18) }, () => 'Too young')
    .otherwise(() => 'Invalid user');

実践的な組み合わせ例

// React Hook FormとZodの組み合わせ
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const FormSchema = z.object({
  name: z.string().min(1, '名前は必須です'),
  email: z.string().email('有効なメールアドレスを入力してください'),
  age: z.number().min(18, '18歳以上である必要があります'),
  agree: z.boolean().refine((val) => val === true, {
    message: '利用規約に同意してください',
  }),
});

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('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      {/* 他のフィールド */}
    </form>
  );
}
// tRPCとZodを使った型安全なAPI
import { z } from 'zod';
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

const userRouter = t.router({
  getById: t.procedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input }) => {
      const user = await db.user.findById(input.id);
      return UserSchema.parse(user);
    }),
    
  create: t.procedure
    .input(
      z.object({
        name: z.string(),
        email: z.string().email(),
        password: z.string().min(8),
      })
    )
    .mutation(async ({ input }) => {
      const hashedPassword = await hash(input.password);
      const user = await db.user.create({
        ...input,
        password: hashedPassword,
      });
      return UserSchema.parse(user);
    }),
});

// クライアント側は自動的に型安全
const user = await trpc.user.getById.query({ id: '123' });
// ZustandとValibotの組み合わせ
import { create } from 'zustand';
import * as v from 'valibot';

const UserStateSchema = v.object({
  user: v.nullable(
    v.object({
      id: v.string(),
      name: v.string(),
      email: v.string(),
    })
  ),
  isLoading: v.boolean(),
  error: v.nullable(v.string()),
});

type UserState = v.InferOutput<typeof UserStateSchema>;

interface UserActions {
  setUser: (user: UserState['user']) => void;
  setLoading: (isLoading: boolean) => void;
  setError: (error: string | null) => void;
}

const useUserStore = create<UserState & UserActions>((set) => ({
  user: null,
  isLoading: false,
  error: null,
  
  setUser: (user) => {
    // バリデーション付きで更新
    const validUser = v.parse(
      UserStateSchema.entries.user,
      user
    );
    set({ user: validUser });
  },
  
  setLoading: (isLoading) => set({ isLoading }),
  setError: (error) => set({ error }),
}));

パフォーマンス比較

Valibot(最速・最軽量) 100 %
完了
手動実装 95 %
io-ts 85 %
Effect 80 %
Zod(機能最多) 70 %
Yup 60 %

*相対的なパフォーマンススコア(バリデーション速度・2025 年ベンチマーク)

選定基準とベストプラクティス

ライブラリ選定のポイント

Valibot

バンドルサイズが重要な場合

Zod

エコシステムとドキュメントが充実

Effect + Zod

複雑なエラーハンドリングが必要な場合

ts-pattern

複雑な条件分岐を扱う場合

ベストプラクティス

  1. スキーマファーストアプローチ

    // スキーマから型を生成
    const UserSchema = z.object({...});
    type User = z.infer<typeof UserSchema>;
  2. エラーメッセージの日本語化

    import { z } from 'zod';
    import { zodI18nMap } from 'zod-i18n-map';
    import translation from 'zod-i18n-map/locales/ja/zod.json';
    
    z.setErrorMap(zodI18nMap);
  3. 段階的な導入

    • 新規 api エンドポイントから導入
    • 重要度の高い機能から優先的に適用
    • チーム全体での知識共有
  4. Typescript 7との連携(2025年新機能)

    // tsgoコンパイラでの最適化
    // 型ガードのパフォーマンスが自動的に向上

注意事項

  • ライブラリのバージョンアップに注意(破壊的変更がある場合)
  • 過度なバリデーションはパフォーマンスに影響
  • サーバーサイドとクライアントサイドで同じスキーマを共有する

まとめ

型ガードライブラリを活用することで、typescript プロジェクトの型安全性と開発効率が大幅に向上します。プロジェクトの規模と要件に応じて、適切なライブラリを選択しましょう。

次のステップ

  1. Zodから始める - 最も人気があり、学習リソースが豊富
  2. パフォーマンスが重要なら - Valibot を検討
  3. 複雑なロジックなら - Effect や ts-pattern を追加

型安全性は、バグの早期発見と開発体験の向上につながります。今日から導入して、より堅牢な typescript コードを書きましょう!

参考文献

📚 さらに学びを深めるために

この記事で紹介したライブラリやテクニックについて、より詳しく学べるリソースをカテゴリ別にまとめました。

📖 公式ドキュメント

🔧 APIリファレンス・実装ガイド

🇯🇵 日本語リソース

🛠️ オンラインツール・Playground

💬 コミュニティ・その他

💡 学習のコツ

  1. まずは公式ドキュメントで基礎を固める
  2. Playground で実際に手を動かして試す
  3. 日本語リソースで理解を深める
  4. コミュニティで最新情報をキャッチアップ

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。