この記事のポイント

  • TypedDictとdataclassの適切な責務分離をレビューで判断できる
  • API受信層・業務層の型構造分離の設計方針を身につける
  • 型安全性と柔軟性を両立した現場設計の指摘技法が学べる

そもそもTypedDictとdataclassとは

TypedDictとは

from typing import TypedDict

class ApiRequest(TypedDict):
    request_id: int
    endpoint: str
    client_ip: str
  • 辞書ベースの構造
  • 動的データ(JSONなど)の受信・検証前処理に有効
  • 辞書操作互換性を維持

dataclassとは

from dataclasses import dataclass

@dataclass
class ApiRequest:
    request_id: int
    endpoint: str
    client_ip: str
  • オブジェクト指向的構造
  • 内部業務処理・ドメインモデル向き
  • 型安全・補完性・リファクタリング耐性高

この2者は役割が明確に異なります。

なぜこれをレビューするのか

現場で頻発する失敗は以下の通りです。

  • 全てをTypedDict化して辞書依存設計崩壊
  • dataclassの利用タイミング誤認識
  • API受信責務と内部処理責務が混在
  • 柔軟性依存による型安全崩壊

レビューアーは「型定義=責務写像」で読み解きます。

レビューアー視点

  • TypedDict導入箇所がAPI受信責務に一致しているか
  • dataclass導入箇所がドメイン処理責務に限定されているか
  • 変換ポイントが整理されているか
  • 動的データ構造依存が業務層へ侵食していないか
  • 型安全保証が各層で実現できているか

開発者視点

  • TypedDictは受信層専用型として割り切る
  • dataclassは処理層・ドメイン層型に限定する
  • 変換責務はAPIアダプター層に集中させる
  • 辞書型のまま内部業務へ流さない
  • 柔軟性より安全性優先で責務整理する

良い実装例

なぜこの実装が良いのか

  • TypedDictはAPI受信責務専用
  • dataclassは内部処理責務専用
  • 変換責務はアダプター層に明示
  • 柔軟性と型安全が自然に両立
# api_schema.py

from typing import TypedDict

class ApiRequestPayload(TypedDict):
    request_id: int
    endpoint: str
    client_ip: str
# domain_model.py

from dataclasses import dataclass

@dataclass
class ApiRequestLog:
    request_id: int
    endpoint: str
    client_ip: str
# converter.py

def to_domain(payload: ApiRequestPayload) -> ApiRequestLog:
    return ApiRequestLog(
        request_id=payload["request_id"],
        endpoint=payload["endpoint"],
        client_ip=payload["client_ip"],
    )
補足

受信層 → 変換層 → 業務層という型責務の分離が実現されています。レビューアーは変換ポイントの明示性を重点確認します。

レビュー観点

  • TypedDictはAPI受信責務専用になっているか
  • dataclassは内部処理責務専用になっているか
  • 辞書操作が業務層へ侵食していないか
  • 変換責務が明示的に管理されているか
  • 柔軟性依存で型安全が崩壊していないか

良くない実装例: ケース1(業務層までTypedDict侵食)

# bad_typed_dict_leak.py

from typing import TypedDict

class ApiRequest(TypedDict):
    request_id: int
    endpoint: str
    client_ip: str

def save_log(data: ApiRequest):
    print(data["request_id"])
@Reviewer
業務層まで辞書型で設計されています。内部処理はdataclass等へ責務分離してください。

問題点

  • 業務層が辞書操作前提設計
  • 型安全性崩壊
  • 補完・リファクタリング耐性消失

改善例

# good_dataclass_domain.py

from dataclasses import dataclass

@dataclass
class ApiRequestLog:
    request_id: int
    endpoint: str
    client_ip: str

def save_log(log: ApiRequestLog):
    print(log.request_id)

辞書型は受信境界線で止めるが原則です。レビュー時は業務層が辞書操作していないかを即確認します。

良くない実装例: ケース2(dataclassで受信柔軟性崩壊)

# bad_dataclass_api_receiving.py

from dataclasses import dataclass

@dataclass
class ApiRequest:
    request_id: int
    endpoint: str
    client_ip: str

def handle_request(data: dict):
    request = ApiRequest(**data)
@Reviewer
受信層でdataclass直接使用は柔軟性・検証性が落ちます。TypedDict経由で受信責務を整理してください。

問題点

  • API受信段階で型固定
  • 柔軟なバリデーション困難
  • エラーハンドリング脆弱

改善例

# good_typed_dict_api_receiving.py

from typing import TypedDict

class ApiRequestPayload(TypedDict):
    request_id: int
    endpoint: str
    client_ip: str

def handle_request(data: ApiRequestPayload):
    # バリデーション・変換責務を分離実装
    pass

TypedDictで受信→検証→変換→dataclassという段階分離設計が安定します。レビューではAPI境界線型設計を重点確認します。

良くない実装例: ケース3(型変換責務の曖昧化)

# bad_implicit_conversion.py

from typing import TypedDict
from dataclasses import dataclass

class ApiRequestPayload(TypedDict):
    request_id: int
    endpoint: str
    client_ip: str

@dataclass
class ApiRequestLog:
    request_id: int
    endpoint: str
    client_ip: str

def save_log(payload: ApiRequestPayload):
    log = ApiRequestLog(**payload)
    print(log.request_id)
@Reviewer
変換責務が業務層に侵入しています。専用変換関数へ責務分離してください。

問題点

  • 業務層内に辞書展開記法が混入
  • 変換責務が境界線崩壊
  • 可読性・保守性低下

改善例

# good_explicit_converter.py

def to_domain(payload: ApiRequestPayload) -> ApiRequestLog:
    return ApiRequestLog(
        request_id=payload["request_id"],
        endpoint=payload["endpoint"],
        client_ip=payload["client_ip"],
    )

def save_log(log: ApiRequestLog):
    print(log.request_id)

型変換責務は専用関数化し、境界線責務を明示が設計原則です。レビューでは変換の混在有無を即時確認します。

良くない実装例: ケース4(Optionalと柔軟辞書型の過剰依存)

# bad_overflexible_dict.py

from typing import TypedDict, Optional

class ApiRequestPayload(TypedDict, total=False):
    request_id: Optional[int]
    endpoint: Optional[str]
    client_ip: Optional[str]
@Reviewer
柔軟性依存で全フィールドOptional化しています。受信必須/任意区分を整理してください。

問題点

  • total=False濫用
  • None混在型設計崩壊
  • 業務責務不明瞭

改善例

# good_typed_dict_mandatory_optional.py

from typing import TypedDict, Optional

class ApiRequestPayload(TypedDict):
    request_id: int
    endpoint: str
    client_ip: str
    archived_at: Optional[str]

Optionalは存在不定責務に限定が基本です。レビューではOptional乱用を即指摘対象にします。

観点チェックリスト

まとめ

TypedDictとdataclassの設計分離は「受信柔軟性 vs 内部型安全」のバランス設計技術そのものです。
レビューアーは常に

  • どこまでが受信責務か?
  • どこからが業務責務か?
  • どこで型を切り替えているか?

を読み解き、型定義を「責務境界図」として読むレビュー力を養っていくことが重要です。
このレビュー技法は現場育成教材として極めて実戦的に機能します。