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の型定義(MetaやStoryObj)は、コンポーネントの型パラメータを正しく推論できない場合があります。特にジェネリクスを使う場合は、明示的な型指定が必要です。
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 }
);
iconとimageのどちらか一方のみ、または両方なしを型で表現できます。実際に検証したところ、両方を同時に渡すと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<typeof Component>"]
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ファイルでargsにas 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設定から始め、チーム全体で型安全性の価値を共有しながら、段階的に高度なパターンを導入していくアプローチが成功しやすいと考えています。型が崩れない運用を継続することで、長期的にメンテナブルなコンポーネントライブラリを構築できます。
関連リンク
著書
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
articleStorybook 品質ゲート運用:Lighthouse/A11y/ビジュアル差分を PR で自動承認
articleStorybook × Design Tokens 設計:Style Dictionary とテーマ切替の連携
articleStorybook Decorators/Parameters 辞典:背景・テーマ・グローバル設定の定石
articleStorybook × Nuxt/SvelteKit/VitePress:マルチフレームワーク併用の初期配線
articleStorybook で Zustand をモックする:Controls 連動とシナリオ駆動 UI
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
article2026年1月13日TypeScriptで型レベル計算の使い方を学ぶ 算術演算を型システムで実装する
article2026年1月13日TypeScriptで実行時バリデーション自動生成を設計する 型と実行時チェックを整合させる
articleNext.js・React Server Componentsが危険?async_hooksの脆弱性CVE-2025-59466を徹底解説
article【緊急】2026年1月13日発表 Node.js 脆弱性8件の詳細と対策|HTTP/2・async_hooks のDoS問題を解説
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
