[Astro] z.enum で型安全なカテゴリ設計にする

Tech
[Astro] z.enum で型安全なカテゴリ設計にする

このブログはカテゴリを5つ(tech / marketing / office / music / life)に固定しています。記事の frontmatter に手で書く以上、タイポや想定外カテゴリが混ざるのが怖いところ。そこを Content Collections の Zod スキーマ z.enumビルド時に弾くようにしました。

スキーマで enum を切る

content.config.tscategoryz.enum([...]) にします。

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    category: z.enum(['tech', 'marketing', 'office', 'music', 'life']),
    // ...
  }),
});

export const collections = { blog };

これで、例えば frontmatter に category: tecn(タイポ)と書くと、ビルドや astro check でエラーになります。

Invalid enum value. Expected 'tech' | 'marketing' | 'office' | 'music' | 'life', received 'tecn'

実行時に気づくのではなく、ビルドが通らないので確実です。

カテゴリ定義を1か所に集約する

ラベルやアイコンなどの付随情報も、site.ts にまとめて持っておくと取り回しが良いです。keyof typeof で型も自動生成します。

// src/data/site.ts
export const CATEGORIES = {
  tech: { label: 'Tech', icon: 'code' },
  marketing: { label: 'Marketing', icon: 'chart' },
  office: { label: 'Office', icon: 'book' },
  music: { label: 'Music', icon: 'music' },
  life: { label: 'Life', icon: 'road' },
} as const;

export type CategoryKey = keyof typeof CATEGORIES;

CategoryKey'tech' | 'marketing' | 'office' | 'music' | 'life' というユニオン型になります。コンポーネントの Props をこれで縛れば、カテゴリを受け取る箇所すべてが型安全に。

---
import type { CategoryKey } from '@/data/site';
interface Props { category: CategoryKey; }
const { category } = Astro.props;
const { label } = CATEGORIES[category]; // 補完も効く
---

ハマりどころ:2か所のリストは手で揃える

z.enum([...])(スキーマ)と CATEGORIES(定義)で、カテゴリのリストが2か所になります。本来は1か所にしたいところですが、z.enum は引数にリテラルの配列を要求するため、Object.keys(CATEGORIES) をそのまま渡すと型が string[] に落ちて enum になりません。

割り切って2か所に書き、「片方を変えたらもう片方も」と決めておくのが現実的でした。どうしても1か所にしたいなら、CATEGORIES のキーから z.enum(Object.keys(CATEGORIES) as [string, ...string[]]) のようにキャストする手もありますが、可読性とのトレードオフです。

メモ

  • z.enumfrontmatter のカテゴリをビルド時に検証できる(タイポ・未定義カテゴリを弾ける)
  • keyof typeof でカテゴリ定義からユニオン型を自動生成、Props もそれで縛る
  • スキーマと定義の2重管理は割り切る(or キャストで1本化)

カテゴリのような「決め打ちの集合」は、enum 化しておくと後々の事故が減って安心でした。

Astro, TypeScript