T-CREATOR

<div />

StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する

2026年1月10日
StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する

「Storybookで型が崩れる」「Controlsと型定義が一致しない」「実行時エラーが頻発する」といった問題は、TypeScriptとStorybookを組み合わせたUI開発でよく遭遇する課題です。本記事では、Props型の設計パターンとControls連携における型安全なアプローチを比較し、実務で型が崩れない運用ルールまで整理します。

初学者の方には「どう設計すれば型安全になるか」を、実務者の方には「チーム開発でどう運用するか」を判断できる内容を提供します。実際の検証結果と失敗から学んだ知見を含めた、すぐに実践できるユースケース集です。

Props型設計アプローチ

アプローチ型安全性学習コスト保守性実務での推奨度
シンプルな型定義小規模プロジェクト向け
条件付きProps状態に応じた制約が必要な場合
排他的Props相互排他的な機能を持つコンポーネント
ジェネリクス活用最高最高再利用性の高い汎用コンポーネント

この表は各アプローチの特徴を簡易的に示したものです。詳細な判断基準は後半で解説します。

検証環境

  • OS: macOS 15.x / Windows 11 / Ubuntu 24.04
  • Node.js: 22.12.0
  • TypeScript: 5.7.2
  • 主要パッケージ:
    • react: 19.0.0
    • storybook: 8.5.3
    • @storybook/react: 8.5.3
    • @storybook/addon-controls: 8.5.3
  • 検証日: 2026 年 01 月 10 日

型安全なUI開発が求められる背景

TypeScriptとStorybookを組み合わせることで、コンポーネント開発における型安全性と開発体験の両立が可能になります。この章では、なぜ型安全なUI開発が必要なのか、技術的背景と実務的背景を整理します。

TypeScriptとStorybookの組み合わせが解決する課題

TypeScriptは静的型付けによって開発時の型エラーを検出しますが、Storybookとの連携が不十分だと、以下のような問題が発生します。

  • Storybookのargs(引数)とコンポーネントのProps型が一致せず、実行時エラーが発生する
  • Controlsで変更可能な値がProps型の制約を超えてしまい、意図しない動作を引き起こす
  • 複数人での開発時に、型定義とStoryの実装が乖離していく

実際に業務で遭遇した例として、variantプロパティに'primary' | 'secondary'という型制約があるにもかかわらず、Storybookのtext control経由で任意の文字列を入力でき、実行時に不正な値が渡されるケースがありました。

実務で発生する具体的な問題

チーム開発では、以下のような場面で型の不整合が顕在化します。

API変更時の影響範囲把握

コンポーネントのProps型を変更した際、すべてのStoryファイルに影響が及ぶかを把握できないと、一部のStoryでのみ実行時エラーが発生します。

新規メンバーのオンボーディング

型定義が曖昧だと、Storyを見てもコンポーネントの正しい使い方が理解できず、学習コストが増大します。

リファクタリングの心理的障壁

型安全性が保証されていないと、Propsの変更がどこに影響するか不明確で、リファクタリングを躊躇してしまいます。

つまずきポイント

Storybookのargsは単なるJavaScriptオブジェクトなので、TypeScriptの型チェックが効きにくい点に注意が必要です。型定義とStoryの実装を常に同期させる運用ルールが不可欠です。

Props型設計における典型的な課題

型安全なUI開発を妨げる典型的な課題を、実際のコード例とともに見ていきます。この章では、どのような設計が型崩れを引き起こすのかを理解し、次章での解決策につなげます。

型定義の不足によるランタイムエラー

Props型を適切に定義しないと、開発時には問題なくてもランタイムでエラーが発生します。

typescript// 型定義が不足している例
export const Button = ({ variant, children, onClick }) => {
  return (
    <button className={`btn-${variant}`} onClick={onClick}>
      {children}
    </button>
  );
};

この例では、variantの型がanyと推論されるため、任意の値を渡せてしまいます。実際に試したところ、Storybookから数値や配列を渡してもTypeScriptエラーにならず、実行時にCSSクラス名が不正になる事象が発生しました。

Controlsと型定義の不整合

StorybookのargTypes設定が型定義と一致していないケースです。

typescriptinterface ButtonProps {
  variant: "primary" | "secondary";
  size: "small" | "large";
}

export default {
  component: Button,
  argTypes: {
    variant: { control: "text" }, // 任意の文字列を入力可能
    size: { control: "text" }, // 型制約が効かない
  },
};

この設定では、ユニオン型で制約しているにもかかわらず、text controlで任意の文字列を入力できてしまいます。検証の結果、variant"danger"を入力しても型エラーにならず、コンポーネント側で想定外の値を受け取る問題が確認されました。

条件付きPropsの型制約不足

特定の条件下でのみ必須となるPropsを、適切に型制約できていない例です。

typescriptinterface AlertProps {
  variant: "info" | "error";
  message: string;
  onRetry?: () => void;
}

// 問題:errorの場合はonRetryが必須だが、型で強制できていない

この設計では、variant'error'の場合にonRetryを必須にしたいが、オプショナルなので型チェックが働きません。業務で実際に、エラー表示時に再試行ボタンが表示されず、ユーザビリティの問題になった経験があります。

ジェネリクスの不適切な使用

型パラメータを持つコンポーネントで、Storybookとの連携が困難になるケースです。

typescriptinterface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

export function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map(renderItem)}</ul>;
}

ジェネリクスを使ったコンポーネントは、Storybookで型推論が効かず、Meta<typeof List>のような型定義が不正確になりがちです。

つまずきポイント

Storybookの型定義(MetaStoryObj)は、コンポーネントの型パラメータを正しく推論できない場合があります。特にジェネリクスを使う場合は、明示的な型指定が必要です。

Props型設計パターンの比較と判断基準

型安全なProps設計には複数のアプローチがあります。この章では、各パターンを比較し、どの状況でどのアプローチを選ぶべきかを判断できるようにします。

以下の図は、コンポーネントの特性に応じた型設計パターンの選択フローを示しています。

mermaidflowchart TD
  start["コンポーネントの<br/>型設計を開始"] --> check1{"Propsの数は<br/>5個以下?"}
  check1 -->|Yes| check2{"条件付きの<br/>制約が必要?"}
  check1 -->|No| check3{"相互排他的な<br/>Propsがある?"}

  check2 -->|No| simple["シンプルな型定義<br/>interface + ユニオン型"]
  check2 -->|Yes| conditional["条件付きProps<br/>状態依存の制約"]

  check3 -->|Yes| exclusive["排他的Props<br/>never型で排他制御"]
  check3 -->|No| check4{"データ型が<br/>可変?"}

  check4 -->|Yes| generic["ジェネリクス<br/>型パラメータで汎用化"]
  check4 -->|No| conditional2["条件付きProps<br/>複雑な制約に対応"]

この図により、自プロジェクトのコンポーネントに適した型設計パターンを選択できます。

シンプルな型定義とインターフェース設計

最も基本的なアプローチは、interfaceを使った明示的な型定義です。

typescriptinterface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'small' | 'medium' | 'large';
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = (props) => {
  return <button {...props} />;
};

このアプローチは学習コストが低く、小規模プロジェクトに適しています。ただし、複雑な条件分岐や相互排他的なPropsには対応しきれません。

向いているケース

  • シンプルなコンポーネント(ボタン、ラベル、アイコンなど)
  • Propsの数が5個以下
  • 条件付きの制約が不要な場合

条件付きPropsパターン

特定のPropの値によって、他のPropsの必須/オプショナルが変わる設計です。

typescripttype BaseAlertProps = {
  message: string;
};

type AlertProps = BaseAlertProps &
  (
    | { variant: "info" | "success"; onRetry?: never }
    | { variant: "error"; onRetry: () => void }
  );

export const Alert: React.FC<AlertProps> = (props) => {
  // propsの型に応じて分岐
};

この設計では、variant'error'の場合にonRetryが必須、それ以外では存在できないことを型で強制します。

向いているケース

  • 状態によって必須項目が変わるコンポーネント
  • フォームやアラートなど、複数のバリエーションを持つUI
  • 型レベルでの制約が重要な場面

排他的Propsパターン

複数のPropsが相互に排他的である場合に使用します。

typescripttype CardProps = {
  title: string;
  description: string;
} & (
  | { icon: React.ReactNode; image?: never }
  | { icon?: never; image: string }
  | { icon?: never; image?: never }
);

iconimageのどちらか一方のみ、または両方なしを型で表現できます。実際に検証したところ、両方を同時に渡すとTypeScriptエラーになり、意図しない状態を防げました。

向いているケース

  • アイコンと画像など、排他的な表示要素を持つコンポーネント
  • 複数の入力方法があるが、同時使用を禁止したい場合

ジェネリクスを活用した再利用可能な型設計

型パラメータを使って、汎用的なコンポーネントを設計します。

typescriptinterface SelectProps<T extends string | number> {
  options: Array<{ value: T; label: string }>;
  value: T;
  onChange: (value: T) => void;
}

export function Select<T extends string | number>(props: SelectProps<T>) {
  // 実装
}

このアプローチは、ドロップダウンやテーブルなど、データ型に依存しないコンポーネントに有効です。

向いているケース

  • データ型が可変なコンポーネント(リスト、テーブル、セレクトボックス)
  • 複数のプロジェクトで再利用するコンポーネントライブラリ
  • 型安全性を最大限保ちたい場合

Props型設計パターンの詳細比較

パターン型安全性実装難易度Storybook連携保守性推奨ケース
シンプルな型定義容易基本的なコンポーネント
条件付きPropsやや難状態依存の制約がある場合
排他的Propsやや難相互排他的な機能
ジェネリクス最高最高汎用コンポーネント

実務では、コンポーネントの複雑さに応じてパターンを使い分けることが重要です。まずはシンプルな型定義から始め、必要に応じて条件付きや排他的Propsに移行する段階的なアプローチを推奨します。

つまずきポイント

条件付きPropsや排他的Propsは、TypeScriptの型システムに慣れていないと理解が難しいため、チーム内でのドキュメント化とコードレビューでの確認が不可欠です。

Storybook Controlsと型定義の連携パターン

StorybookのargTypes設定とTypeScriptの型定義を適切に連携させることで、型安全なコンポーネント開発が実現します。この章では、Controls設定の具体的なパターンを比較します。

以下の図は、TypeScriptの型定義からStorybookのControlsへ連携する流れを示しています。

mermaidflowchart LR
  props["Props型定義<br/>interface"] --> meta["Meta型定義<br/>Meta&lt;typeof Component&gt;"]
  meta --> argTypes["argTypes設定<br/>手動 or 自動推論"]
  argTypes --> controls["Storybook Controls<br/>select / radio / text"]
  controls --> story["Story定義<br/>StoryObj型"]
  story --> component["コンポーネント<br/>型チェック済みProps"]

  style props fill:#e1f5ff
  style meta fill:#fff4e1
  style argTypes fill:#ffe1f5
  style controls fill:#e1ffe1
  style story fill:#f5e1ff
  style component fill:#e1f5ff

型定義を起点に、Storybookの各レイヤーで型安全性を保つことが重要です。

手動argTypes定義とselect controlの活用

最も確実な方法は、argTypesを手動で定義し、ユニオン型と一致させることです。

typescriptimport type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  component: Button,
  argTypes: {
    variant: {
      control: { type: "select" },
      options: ["primary", "secondary", "danger"],
    },
    size: {
      control: { type: "radio" },
      options: ["small", "medium", "large"],
    },
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

この設定により、Controlsから不正な値を入力できなくなります。検証の結果、select controlにすることで、型定義と完全に一致した値のみを選択可能になりました。

型推論を活用した自動Controls生成

Storybook 7以降では、TypeScriptの型定義から自動的にControlsを生成できます。

typescriptconst meta = {
  component: Button,
  // argTypesを省略しても型から推論される
} satisfies Meta<typeof Button>;

satisfiesを使うことで、型推論を活かしつつ型チェックも行えます。ただし、実際に試したところ、ユニオン型のControlsが自動でselectにならず、手動設定が必要な場合がありました。

Controls設定パターンの比較

設定方法型安全性保守コスト推奨度
手動argTypes + select高(型とoptionsの二重管理)ユニオン型の場合推奨
型推論 + satisfiesシンプルな型の場合推奨
自動生成のみ最低非推奨

実務では、ユニオン型のPropsは必ず手動でselectまたはradio controlを設定し、プリミティブ型は型推論に任せるハイブリッドアプローチが有効です。

CSF3.0形式での型安全なStory定義

Component Story Format 3.0を使った、型安全なStory定義の例です。

typescriptexport const Primary: Story = {
  args: {
    variant: "primary", // 型チェックが効く
    size: "medium",
    children: "Primary Button",
  },
};

Story型はStoryObj<typeof meta>から推論されるため、argsの型が自動的にチェックされます。業務で実際に活用したところ、存在しないPropを指定するとエラーになり、リファクタリング時の見落としを防げました。

つまずきポイント

Meta型とStoryObj型の使い分けが初学者には難しいポイントです。MetaはStorybook全体の設定、StoryObjは個別Storyの型として使い分けます。

型が崩れない運用ルールとチーム開発

型安全性を保つには、技術的な設計だけでなく、チーム全体での運用ルールが重要です。この章では、実務で効果のあった運用方法を紹介します。

Props型とStory定義を同一ファイルに配置しないルール

Propsのインターフェースとコンポーネント実装をButton.tsxに、StoryをButton.stories.tsxに分離します。

csssrc/
  components/
    Button.tsx        # コンポーネントとProps型
    Button.stories.tsx # Storybook定義

この構成により、型定義の変更がStoryファイルに即座に反映され、型エラーで気づけるようになります。実際に運用したところ、Props変更時の影響範囲が明確になり、レビュー時のチェックが容易になりました。

ESLintルールによる型安全性の強制

TypeScript ESLintで、型安全性を損なう記述を禁止します。

json{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-unsafe-assignment": "error",
    "@typescript-eslint/no-unsafe-member-access": "error",
    "@typescript-eslint/strict-boolean-expressions": "warn"
  }
}

any型の使用を禁止することで、Props定義での型の抜け穴を防ぎます。検証中、Storyファイルでargsas anyでキャストしていた箇所を発見し、修正できました。

型定義の共通化とエイリアス設定

頻出する型をまとめた共通型定義ファイルを作成します。

typescript// types/props.ts
export interface BaseProps {
  className?: string;
  "data-testid"?: string;
}

export type Variant = "primary" | "secondary" | "danger";
export type Size = "small" | "medium" | "large";

コンポーネント間で型を共有することで、一貫性が保たれます。業務では、Variant型を統一したことで、デザインシステムとの整合性が向上しました。

CI/CDでの型チェック自動化

Pull Request時に型チェックを必須にします。

json{
  "scripts": {
    "type-check": "tsc --noEmit",
    "storybook:type-check": "tsc --noEmit --project .storybook"
  }
}

GitHub ActionsなどのCIでtype-checkを実行し、型エラーがあればマージできないようにします。実際に導入したところ、型崩れを起こしたPRが自動的に検出され、レビュー負荷が大幅に減少しました。

失敗から学んだ運用上の注意点

業務で遭遇した失敗例として、以下があります。

Storyファイルでの型アサーションの乱用

argsに不正な値を渡すためにas anyを使ってしまい、型安全性が崩れました。代わりに、Propsの型定義を見直すことで解決しました。

argTypesとPropsの型定義の乖離

手動でargTypesを定義した際、Propsの型を変更してもargTypesを更新し忘れるケースが頻発しました。型定義を変更したら、必ず対応するStoryファイルも確認するチェックリストを導入して改善しました。

つまずきポイント

チーム全体で型安全性の重要性を共有しないと、一部のメンバーがanyを使って回避してしまう問題が起きます。定期的な勉強会とコードレビューでの指摘が有効です。

実装パターン別の型安全設計まとめ

ここまでの内容を踏まえ、実装パターン別に推奨される型安全設計をまとめます。

基本コンポーネントの推奨設計

ボタンやラベルなどのシンプルなコンポーネントには、以下のアプローチが適しています。

  • Props型はinterfaceでユニオン型を使った制約
  • argTypesでselect/radio controlを明示的に設定
  • React.FCで型を明示
typescriptinterface ButtonProps {
  variant: 'primary' | 'secondary';
  children: React.ReactNode;
}

export const Button: React.FC<ButtonProps> = ({ variant, children }) => (
  <button className={`btn-${variant}`}>{children}</button>
);

複雑な状態を持つコンポーネントの推奨設計

フォームやモーダルなど、複雑な状態を持つコンポーネントには、条件付きPropsが有効です。

  • ユニオン型で状態ごとにPropsを分岐
  • 型ガードを使った安全な分岐処理
  • Storyは状態ごとに分けて定義
typescripttype ModalProps =
  | { isOpen: true; onClose: () => void; title: string }
  | { isOpen: false; onClose?: never; title?: never };

汎用コンポーネントの推奨設計

データテーブルやリストなど、データ型に依存しないコンポーネントには、ジェネリクスを活用します。

  • 型パラメータで柔軟性を確保
  • Storyでは具体的な型を指定
  • Meta型で明示的に型パラメータを設定
typescriptinterface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

export function List<T>(props: ListProps<T>) {
  // 実装
}

実装パターン別推奨アプローチの詳細比較

コンポーネント種別推奨型設計argTypes設定Story分割保守難易度
基本コンポーネントinterface + ユニオン型手動selectバリエーション別
複雑な状態を持つ条件付きProps手動select + 型ガード状態別
汎用コンポーネントジェネリクス型パラメータ指定データ型別

この比較表を参考に、自プロジェクトのコンポーネントに適したアプローチを選択してください。実務では、まず基本コンポーネントで型安全性を確立し、段階的に複雑なパターンに対応していくのが現実的です。

つまずきポイント

最初から完璧な型設計を目指すと、学習コストが高く挫折しやすいです。まずはシンプルな型定義から始め、チームのスキルレベルに応じて段階的に高度なパターンを導入しましょう。

まとめ

TypeScriptとStorybookを組み合わせた型安全なUI開発は、Props型の設計とControls連携を適切に行うことで実現します。本記事で紹介したパターンと運用ルールは、プロジェクトの規模やチームのスキルレベルに応じて選択してください。

型安全性は、一度確立すれば開発効率とコード品質の両方を向上させます。ただし、過度に複雑な型設計は保守コストを高めるため、必要十分な型制約にとどめることが重要です。

実務では、まず基本的な型定義とargTypes設定から始め、チーム全体で型安全性の価値を共有しながら、段階的に高度なパターンを導入していくアプローチが成功しやすいと考えています。型が崩れない運用を継続することで、長期的にメンテナブルなコンポーネントライブラリを構築できます。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;