💭

TSKaigi Hokuriku参加録 ~責務分離の観点から学んだこと~

に公開

こんにちは、新卒2年目のはよちゃんです。2回目の投稿になります。

先週の11月23日、TSKaigi Hokuriku に参加してきました!

色々な学びがありましたが、今回の記事では個人的に関心があったフロントエンドの責務分離について、まとめることにしました。少しでも参考になれば幸いです。

TSKaigiとは

TypeScript に関する幅広いテーマを扱う国内最大級の技術カンファレンスです。
今年の5月には東京都・神田でも開催されていました。

対象者

  • UI と API の境界が曖昧なまま実装が進んでしまっている方
  • コンテナコンポーネントが肥大化し、整理に悩んでいる方
  • React/フロントエンドの責務分離に興味がある方

目次

  1. フロントエンドにおける「型」の責務分離に対する1つのアプローチ
  2. フロントエンドアーキテクチャの設計方法論 Feature-Sliced Designの紹介
  3. 4分でわかった気になるRailway Oriented Programming

1. フロントエンドにおける「型」の責務分離に対する1つのアプローチ

資料

https://speakerdeck.com/kinocoboy2/hurontoendoniokeru-xing-noze-ren-fen-jie-nidui-suru1tunoapuroti

概要

フロントエンド開発では、UIとAPIが異なる性質を持つ値を扱うため、
型定義とバリデーションが複雑化しやすい
です。特に文字列と数値の混在、暫定的なUnion型、
UIとAPI型の流用、分散したバリデーション処理が典型的な問題となります。

この問題に対して本セッションでは、UI・ロジック・ドメインを分離するために、
「ICONIXプロセス」を用いるアプローチが紹介されていました。

ICONIXプロセス(boundary ↔ control ↔ entity)とは?

フロントエンドのデータ処理を 3つの責務に分割し、
UI とビジネスロジックの混在を防ぐための設計パターンです。

boundary(UI・入出力)

ユーザー入力や表示を扱う層です。

  • フォームの入力値(string)
  • useState やコンポーネントの props
  • 画面表示のために必要な値

UI の都合のままの型(例: "1""")をそのまま持ちます。
ここではビジネスルールの判定は行いません。

control(変換・バリデーション)

boundary の値と entity の値を変換する層です。

  • UI の入力値をドメイン型へ変換する
  • parseInt, trim などの前処理を行う
  • ビジネスルールに基づいたバリデーションを行う

UI とドメインの間で、
不正な値はエラーとして返し、正しい値だけを entity に渡します。

entity(ドメインモデル)

アプリケーションが扱うドメインの値を表す層です。

  • number / boolean / enum などに正規化された型
  • API 仕様に沿ったペイロード
  • ビジネスルールを満たした値オブジェクト

UI での入力形式は考慮せず、
「すでに正しい値だけが入ってくる」という前提で実装します。

本セッションで示されたポイント

  • UIステート型とドメインモデル型を分離する
  • 型変換・バリデーションを control 層に隔離する
  • 状態管理は状態に応じた型制約を与え安全性を高める
  • ICONIXプロセスにより、コードの散在を防ぎ構造的に複雑性を減らせる

これらを通して、複雑性を構造的に整理し、
再利用可能で保守性の高い設計を実現する方法
が解説されていました。

確かに、boundary ↔ control ↔ entity で分割すると、
UIとドメインの影響を各層で抑えられ、汎用的で整理された形にできますね!

// Control 層が受け取るUI型
export interface TrainingHoursViewModel {
  weekdayHours?: string;
  weekendHours?: string;
}

// Control 層が返すドメイン型
export interface TrainingHoursPayload {
  weekdayHours: number;
  weekendHours: number;
}

// control層の例(トレーニング時間をドメイン型へ変換)
export const toTrainingHoursPayload = (
  state: TrainingHoursViewModel
):TrainingHoursPayload => {
  const weekday = parseInt(state.weekdayHours, 10);
  if (isNaN(weekday) || weekday <= 0) {
    throw new Error("平日の平均トレーニング時間を正しく入力してください");
  }

  const weekend = parseInt(state.weekendHours, 10);
  if (isNaN(weekend) || weekend <= 0) {
    throw new Error("休日の平均トレーニング時間を正しく入力してください");
  }

  return {
    weekdayHours: weekday,
    weekendHours: weekend,
  };
};

// fetch例
const data = await postTrainingHours(toTrainingHoursPayload(form));

2. フロントエンドアーキテクチャの設計方法論 Feature-Sliced Designの紹介

資料

https://speakerdeck.com/motikoma/tskaigi-hokuriku-2025-hurontoentoakitekutiyanoshe-ji-fang-fa-lun-feature-sliced-designnoshao-jie

概要

従来は「ページ単位でのデータ取得」と「機能単位でのUI分割」を組み合わせて開発していましたが、
規模が大きくなるにつれて、API通信・状態管理・データ加工・UI処理が1つのContainerコンポーネントに集中し、コードの肥大化、見通しの悪化、影響範囲の把握しづらさが生じていました。

これを解消するには、

  • API通信
  • 状態管理
  • ドメインロジック
  • UI整形ロジック

といった責務を明確に分離し、単一責務・低結合・高凝集の単位へと再構成する必要があります。

Feature-Sliced Design は、この責務分離を実現するために、Layers / Slices / Segments の3軸で依存関係を整理する設計思想であり、複雑な業務フロントエンドでもスケール可能なアーキテクチャを実現できます。

3. 4分でわかった気になるRailway Oriented Programming

資料

https://speakerdeck.com/yukishima/4fen-jian-tewakatutaqi-ninarurailway-oriented-programming-50101abc-b17e-4802-b36b-0b0fccc077c4

概要

Railway Oriented Programming(以下、ROP)は、処理の成功・失敗を Result 型で表現し、
バリデーション・在庫チェック・権限判定などのビジネスロジックを「レール」に見立てて連結する設計手法です。

  • 成功時:緑のレール(Success)
  • 失敗時:赤いレール(Failure)

という二つのレールを切り替えながら処理が進むため、
if 文や try-catch に依存せず、処理を安全に連鎖させられる点が特徴です。

発表を受けて学んだこと

ROP の発想をフロントエンドに持ち込むと、
UI・API・ドメインロジックを自然に切り分けやすくなりますね!

特にデータ入力フォームのバリデーションでは 複数エラーを安全に合成できるため、
型で保証される「見通しの良さ」「責務の分離」が得やすくなります。

ここでは、「1. フロントエンドにおける型の責務分離」で用いた Control 層のコードに対し、
ROPの Result 型を適用した例を示します。

// 複数エラーを扱えるよう errors: E[] を持ちます
type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; errors: E[] };

type TrainingHoursError =
  | "平日の平均トレーニング時間を正しく入力してください"
  | "休日の平均トレーニング時間を正しく入力してください";

Control 関数への適用

この関数は UI から分離した入力値変換ロジックです。
Result 型にすることで、UI 側は 成功パスかエラーパスかのどちらかだけに集中できます。

// Control層(ユーザが入力したトレーニング時間をドメイン用に変換)
export const toTrainingHoursPayload = (
  state: TrainingHoursViewModel
): Result<TrainingHoursPayload, TrainingHoursError> => {
  const errors: TrainingHoursError[] = [];

  const weekday = parseInt(state.weekdayHours, 10);
  if (isNaN(weekday) || weekday <= 0) {
    errors.push("平日の平均トレーニング時間を正しく入力してください");
  }

  const weekend = parseInt(state.weekendHours, 10);
  if (isNaN(weekend) || weekend <= 0) {
    errors.push("休日の平均トレーニング時間を正しく入力してください");
  }

  if (errors.length > 0) {
    return { ok: false, errors };
  }

  return {
    ok: true,
    value: {
      weekdayHours: weekday,
      weekendHours: weekend,
    },
  };
};

Result を汎用的に扱う関数

UI から見ると「成功か失敗か」の二択で十分なため、
handleResult は UI ロジックに重複を持たせないためのヘルパー関数として使えます。

export function handleResult<T, E>(
  result: Result<T, E>,
  onSuccess: (value: T) => void,
  onError: (errors: E[]) => void
): void {
  if (result.ok) {
    onSuccess(result.value);
  } else {
    onError(result.errors);
  }
}

UI(呼び出し側)

UI は Result の成否に応じて表示を制御するだけでよく、
ドメインロジックに関与する必要はありません。

handleResult(
  toTrainingHoursPayload(form),
  (payload) => {
    // 成功時のみAPIを呼ぶ
  },
  (errors) => {
    setError({
      weekdayHours: errors.includes("平日の平均トレーニング時間を正しく入力してください")
        ? "平日の平均トレーニング時間を正しく入力してください"
        : null,
      weekendHours: errors.includes("休日の平均トレーニング時間を正しく入力してください")
        ? "休日の平均トレーニング時間を正しく入力してください"
        : null,
    });
  }
);

まとめ

TSKaigi Hokurikuから、フロントエンドが抱えやすい複雑性をどのように整理し、責務を分離するかという点で多くの示唆が得られました。UI・control・entity のICONIXプロセス、Feature-Sliced Design、そして Result 型を用いた ROP といった手法は、どれも「UI に余計な責務を持たせない」「ドメインロジックを明確にする」という共通の方向性を持っています。今回学んだ内容を今後のアプリ開発に取り入れながら、より読みやすく保守しやすいフロントエンドを実現していきたいと思います!!

Discussion