Slide 1

Slide 1 text

© kaonavi, inc. 1 フロントエンドにおける「型」の責 任分解 に対する1つのアプローチ kinocoboy

Slide 2

Slide 2 text

自己紹介 © kaonavi, inc. 2 kinocoboy | キノシタ ヒロキ 所属: 株式会社カオナビ 趣味: プラモ / ギター プライベート : 2児の父 / 北海道出身・千葉在住 好きだったライブラリ : Riot.js , Meteor.js (わかる人いますか...?)

Slide 3

Slide 3 text

© kaonavi, inc. こんな悩みを持つ方へ 3 この登壇のターゲット 型定義が汚染されて困っているエンジニア

Slide 4

Slide 4 text

フロントエンドの型定義 どうやって整理したらいいの? © kaonavi, inc. 4

Slide 5

Slide 5 text

© kaonavi, inc. 🤔 設計の迷い ● UIの要求とAPIの要求が違う ● バリデーション処理があちこちに散ら ばる ● どっちに合わせるべき? >フロントエンドの型定義、どうやって整理したらいいの? 5 よくある悩み 🤔 😰 現実のコードベース ● string | number だらけの型定 義 ● フォームからAPIまで同じ型を使い回 し ● この型、どこで使うんだっけ? 問題提起:なぜフロントエンドでは型定義がこんなに複雑になるのか?

Slide 6

Slide 6 text

解決するために学んだ Clean Architecture © kaonavi, inc. 6

Slide 7

Slide 7 text

© kaonavi, inc. Clean Architectureとは ビジネスルールを外部の詳細(UI、DB、フレー ムワーク)から独立させる設計原則 7 Clean Architecture を知ってみる 核心原則 依存性は内側に向かって流れる(外側は内 側に依存するが、内側は外側に依存しな い)

Slide 8

Slide 8 text

教科書通りには Clean Architectureは 通用しなかった © kaonavi, inc. 8

Slide 9

Slide 9 text

© kaonavi, inc. 9 フロントエンドで「教科書通り」が難しい理由 🎨 フロントエンドでの Clean Architecture 外側からの要求も強い(トップダウン圧力) ● UI(ユーザー体験)が重要 ● API(システム要求)も重要 ● 両方に最適化が必要 🏗 バックエンドでの Clean Architecture 内側から外側へ設計(ボトムアップ) ● Entity(ビジネスルール)が中心 ● Domain Model がすべての基準 ● DB スキーマも Entity に従う 課題:フロントエンドは「ユーザーの都合」と「システムの都合」の 2つの外部からの圧力を受ける

Slide 10

Slide 10 text

なぜフロントエンドでは 型定義がこんなに複雑になるのか? © kaonavi, inc. 10

Slide 11

Slide 11 text

© kaonavi, inc. 11 結論:「ダブルスタンダード」の存在 核心 フロントエンドは「ユーザー(UI)」と「システム(API/DB)」という 2つの異なるプリミティブな要望を繋ぐ場所 だから。 📋今日話すこと: 1. レストランの例え話 で見る「ダブルスタンダード」 2. 「行間」に潜む UI State の正体 3. 設計概念 と TypeScript で作る「境界線」の実装

Slide 12

Slide 12 text

© kaonavi, inc. 12 根本的な「二重性」 に対するレストランの事例

Slide 13

Slide 13 text

© kaonavi, inc. 🤤 腹ペコ (User) お金を払って、 料理(体験)を得たい 13 🍽レストランの登場人物 󰞽 シェフ (System) 料理を提供して、 対価を得たい 重要な関係性:ユーザーとシステムはお互いが顧客である ⚡

Slide 14

Slide 14 text

© kaonavi, inc. 関心事:効率、特定、計算 「オムレツ」という文字より id: 1 が欲しい 14 󰞽システム(シェフ)が見ている世界 interface Request { foodId: 1, // ID で特定 count: 1 // 数値で計算 } 🔍 特徴 ● 暗黙知が多く、無駄がない ● Strict なデータ構造 ● 効率性とパフォーマンスを重視

Slide 15

Slide 15 text

© kaonavi, inc. 関心事:不安解消、選択、理解 id: 1 と言われても分からない。 「オムレツ」「1000円」「美味しそうな写真」が必要 15 🤤 ユーザー(腹ぺこ)が見ている世界 interface Food { id: number, name: "オムレツ", yen: 1000, description: "ふわふわです" } 🎨 特徴 ● システムが知っている情報(価格など)も含める ● 冗長でも親切な提示が必要 ● ユーザーの不安と迷いを解消

Slide 16

Slide 16 text

© kaonavi, inc. 🎨 UI の都合 ユーザーは入力を間違えるし、一時的に空にする 16 ⚡摩擦の発生源「入力フォーム」 🖥 Domain の都合 システムは正確な数値が欲しい 失敗パターン:この 2つを混ぜて 「Partial」 のような型をコンポーネントから APIクライア ントまで引き回してしまう ⚔ 具体例:注文時の「個数」入力 quantity: string | number // 空文字許容、全角数字対応など quantity: number

Slide 17

Slide 17 text

© kaonavi, inc. 17 🕳行間にあるもの = UI State 気づき: システムが必要とする情報( Request)と、ユーザーが必要とする情報( View Model)の間には「行間」がある 🎭行間の正体: ● 変換待ちの状態: 入力中の曖昧なデータ(バリデーション前) ● 状況の状態: 「料理待ち」「お釣り待ち」「注文送信中」 type UserContext = | { status: 'IDLE' } | { status: 'ORDERING', form: OrderFormState } | { status: 'WAITING_FOOD', order: OrderPayload } | { status: 'EATING' };

Slide 18

Slide 18 text

© kaonavi, inc. Iconixプロセスとは 最小限のUMLを用いる、ユースケースを中心 とした設計で、保守性が高く要求を満たすコー ドを実現する手法 18 🏗ICONIX プロセスで整理する Use case Model 矛盾

Slide 19

Slide 19 text

© kaonavi, inc. Robustness Diagram による境界の発見 19 🏗ICONIX プロセスで整理する Boundary (画面) ユーザーに優しく String許容、リッチな情報 Control (ロジック) 変換する場所 曖昧さ → 厳密さ Entity (ドメイン) システムに厳しく Strict、正規化 この問題をユースケース駆動 で捉え直す UI Component 状態 Controller 変換層 Usecase ビジネス Domain Model 純粋な型 🔑

Slide 20

Slide 20 text

© kaonavi, inc. 20 🎯「型」による関心ごと分離の実装

Slide 21

Slide 21 text

© kaonavi, inc. 21 🎯型定義の分離 ❌Bad: 1つの型を使い回す ✅Good: 明確に分ける interface Order { // Domain が汚染されている count: number | string; } // UI State (User の都合) interface OrderFormState = { // 空文字 OK countInput: string; selectedMenuId: number | null; }; // Domain Model (System の都合) interface OrderPayload = { foodId: number; count: number; };

Slide 22

Slide 22 text

© kaonavi, inc. 22 🔄変換ロジック( Mapper)の実装 「行間」を埋める関数を作る。ここでバリデーションと型変換を行う。 const toPayload = (state: OrderFormState): OrderPayload => { const count = parseInt(state.countInput, 10); if (isNaN(count)) throw new Error("数量を入れてね"); if (!state.selectedMenuId) throw new Error("メニューを選んでね"); return { foodId: state.selectedMenuId, count: count }; }; 🎉 メリット UI の変更(デザイン変更) とDomain の変更(API 変更)が互いに影響しなくなる

Slide 23

Slide 23 text

© kaonavi, inc. 23 🏪 状態管理による関心ごと分離の実装

Slide 24

Slide 24 text

© kaonavi, inc. // UI State (フォームの状態) interface OrderFormState { // UI都合: 空文字許容 countInput: string; selectedMenuId: number | null; // UI都合: ローディング状態 isSubmitting: boolean; } // Domain Model (API通信用) interface OrderPayload { // System都合: 厳密な型 foodId: number; count: number; } Reduxによる実装 24 🏪状態管理による関心ごと分離の実装   // Action で変換を明示   const submitOrder = createAsyncThunk(    'order/submit',    async (formState: OrderFormState) => {    // 中間層での変換処理    const payload =       convertFormToPayload(formState);    return await api.submitOrder(payload);    }   );

Slide 25

Slide 25 text

© kaonavi, inc. // UI State (フォームの状態) interface OrderFormState { // UI都合: 空文字許容 countInput: string; selectedMenuId: number | null; // UI都合: ローディング状態 isSubmitting: boolean; } // Domain Model (API通信用) interface OrderPayload { // System都合: 厳密な型 foodId: number; count: number; } Reduxによる実装 25 🏪状態管理による関心ごと分離の実装   // Action で変換を明示   const submitOrder = createAsyncThunk(    'order/submit',    async (formState: OrderFormState) => {    // 中間層での変換処理    const payload =       convertFormToPayload(formState);    return await api.submitOrder(payload);    }   ); Boundary Entity Control

Slide 26

Slide 26 text

© kaonavi, inc. // UI State atoms (フォームの状態 ) const countInputAtom = atom(''); const selectedMenuIdAtom = atom(null); const isSubmittingAtom = atom(false); // Derived atom で変換処理を分離 const orderPayloadAtom = atom((get) => { const countInput = get(countInputAtom); const menuId = get(selectedMenuIdAtom); // バリデーションと変換をここで実施 if (!menuId || !countInput) return null; const count = parseInt(countInput, 10); if (isNaN(count)) return null; return { foodId: menuId, count }; }); Jotaiによる実装 26 🏪状態管理による関心ごと分離の実装 // API連携は別のatomで管理 const submitOrderAtom = atom(null, async (get, set) => { const payload = get(orderPayloadAtom); if (!payload) throw new Error('Invalid form'); set(isSubmittingAtom, true); const result = await api.submitOrder(payload); set(isSubmittingAtom, false); return result; });

Slide 27

Slide 27 text

© kaonavi, inc. // UI State atoms (フォームの状態 ) const countInputAtom = atom(''); const selectedMenuIdAtom = atom(null); const isSubmittingAtom = atom(false); // Derived atom で変換処理を分離 const orderPayloadAtom = atom((get) => { const countInput = get(countInputAtom); const menuId = get(selectedMenuIdAtom); // バリデーションと変換をここで実施 if (!menuId || !countInput) return null; const count = parseInt(countInput, 10); if (isNaN(count)) return null; return { foodId: menuId, count }; }); Jotaiによる実装 27 🏪状態管理による関心ごと分離の実装 // API連携は別のatomで管理 const submitOrderAtom = atom(null, async (get, set) => { const payload = get(orderPayloadAtom); if (!payload) throw new Error('Invalid form'); set(isSubmittingAtom, true); const result = await api.submitOrder(payload); set(isSubmittingAtom, false); return result; }); Boundary Entity Controll Control

Slide 28

Slide 28 text

© kaonavi, inc. 28 🎭 状態(Status)の型定義

Slide 29

Slide 29 text

© kaonavi, inc. 29 🎭状態(Status)の型定義 データだけでなく「状況」も型にする type UserContext = | { status: 'IDLE' } | { status: 'ORDERING', form: OrderFormState } | { status: 'WAITING_FOOD', order: OrderPayload } | { status: 'EATING' }; // 使用例 const handleUserAction = (context: UserContext) => { switch (context.status) { case 'ORDERING': // ここでは form にアクセス可能 return validateForm(context.form); case 'WAITING_FOOD': // ここでは order にアクセス可能 return trackOrder(context.order); // ... } }; 効果:タグ付き Union型を使うことで、今の状態であり得ないデータを物理的にアク セス不可にする

Slide 30

Slide 30 text

© kaonavi, inc. 30 📝今日のまとめ フロントエンドは「ユーザーの都合」と「システムの都合」の交差点である 1. これを混同すると設計が破綻する(ダブルスタンダードの無視) 2. UI State(曖昧)とDomain Model(厳密)の間に明確な境界線(変換層)を引こう 3. 「行間」を埋めるのがフロントエンドエンジニアの腕の見せ所 😋 UI State ユーザーフレンドリー 󰳏 Domain Model システムフレンドリー 🤵

Slide 31

Slide 31 text

© kaonavi, inc. 31 🚀Next Action 💡アクションプラン 1. 既存の型定義を「UI用」「API用」に分類 2. Adapter層(変換関数)を1つ作ってみる 3. Tagged Union型で状態管理を改善 それは「UI State」と「Domain Model」が混ざっているサインかもしれません まずは型を2つに分けることから始めましょう 皆さんのコードベースを見直してみてください ● string | number や Optional だらけの型 ● コンポーネントからAPIクライアントまで同じ型を使い回している コード ● フォームのバリデーションがあちこちに散らばっている コード

Slide 32

Slide 32 text

宣伝 © kaonavi, inc. 32

Slide 33

Slide 33 text

We are hiring!! https://corp.kaonavi.jp/recruit/recruitment/ © kaonavi, inc. 33

Slide 34

Slide 34 text

© kaonavi, inc. 34 ご清聴ありがとうございました