この記事のポイント

  • TypeVarとジェネリクスの設計技法をレビューできる
  • 汎用化しすぎた設計崩壊パターンを見抜ける
  • 型安全と汎用性の適切なバランス感覚を身につける

そもそもTypeVarとGenericsとは

Pythonの型ヒントでは、汎用的な型パラメータを表現するためにTypeVarとジェネリクスが用意されています。

from typing import TypeVar, Generic

T = TypeVar('T')

def identity(value: T) -> T:
    return value
  • TypeVar:型パラメータ宣言
  • Generics:クラス全体の型汎化

非常に強力ですが、汎用性>型安全に陥りがちな設計崩壊ポイントでもあります。

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

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

  • 汎用性追求のあまり型安全崩壊
  • 実質Any化するTypeVar利用
  • 型制約の不足
  • 業務ドメインから遊離した抽象汎用API

レビューアーは 「ジェネリクスを本当に要する場面か?」 を常に設計レベルから読みます。

レビューアー視点

  • 汎用化理由が設計意図と一致しているか
  • TypeVar制約が適切に設計されているか
  • 呼び出し側の型安全性が確保されているか
  • 汎用APIが業務文脈に対して過剰抽象化していないか
  • 制約付きTypeVarの利用検討が行われているか

開発者視点

  • 業務で必要な共通構造だけを汎用化対象にする
  • 型制約は必ず検討する
  • 汎用性よりも呼び出し側の型安全優先
  • ドメイン層に汎用抽象を引き上げすぎない
  • Anyに逃げずTypeVarで安全性担保する

良い実装例

なぜこの実装が良いのか

  • 汎用APIの責務が「重複コード抑止」に限定
  • TypeVar制約によりAPI利用時の型安全が担保
  • ドメイン層では汎用APIを意識せず使える
  • 汎用性と安全性のバランスが自然
# repository.py

from typing import TypeVar, Generic, List

T = TypeVar('T')

class Repository(Generic[T]):
    def __init__(self):
        self._items: List[T] = []

    def add(self, item: T) -> None:
        self._items.append(item)

    def get_all(self) -> List[T]:
        return list(self._items)
# usage.py

from repository import Repository

class ApiRequestLog:
    def __init__(self, request_id: int, endpoint: str):
        self.request_id = request_id
        self.endpoint = endpoint

log_repo = Repository[ApiRequestLog]()
log_repo.add(ApiRequestLog(1, "/api/test"))
all_logs = log_repo.get_all()
補足

「ジェネリクスは利用者が意識しなくてよいAPI提供側の内部実装」が基本設計方針です。TypeVarで利用時の型安全性も守られています。

レビュー観点

  • TypeVar導入理由が設計説明可能か
  • 制約付きTypeVar活用検討が行われているか
  • Any回避が徹底できているか
  • 業務文脈に汎用抽象が過剰侵食していないか
  • 呼び出し側が型安全に利用できるか

良くない実装例: ケース1(無制約TypeVar濫用でAny化)

# bad_typevar_any_like.py

from typing import TypeVar

T = TypeVar('T')

def print_item(item: T):
    print(item)
@Reviewer
TypeVarの導入意図がなく、実質Anyと同義になっています。型制約または汎用性の整理が必要です。

問題点

  • TypeVarの型制約設計が存在しない
  • 任意型受け入れで型安全性崩壊
  • TypeVarを導入した意味が形骸化

改善例

# good_specific_type.py

def print_item(item: str):
    print(item)

TypeVarは汎用性の必然性がある場面にのみ投入します。レビューでは「そもそもジェネリクスが必要だったか?」を常に問います。

良くない実装例: ケース2(Union化による汎用崩壊)

# bad_union_generic.py

from typing import Union

def process_data(data: Union[str, int, float, None, bool]):
    print(data)
@Reviewer
汎用性をUnionで吸収しており型安全性を著しく損なっています。TypeVar活用または業務区分整理が必要です。

問題点

  • Union肥大化による型爆発
  • 呼び出し側が常に型判定負荷を抱える
  • ジェネリクス導入の失敗パターン典型例

改善例

# good_generic_typevar.py

from typing import TypeVar

T = TypeVar('T', str, int, float)

def process_data(data: T):
    print(data)

Union肥大をTypeVarによる制約付きジェネリクスに置き換え、安全性を維持しつつ汎用性も残しています。

良くない実装例: ケース3(業務ドメインまで抽象侵食)

# bad_generic_invasion.py

from typing import TypeVar, Generic

T = TypeVar('T')

class Service(Generic[T]):
    def execute(self, data: T) -> None:
        print(data)
@Reviewer
業務層のServiceまで汎用抽象にしておりドメイン責務が不明瞭です。具体型優先で整理してください。

問題点

  • 業務ドメイン層まで汎用型が侵食
  • ビジネス責務表現が抽象消失
  • 可読性と保守性が大幅低下

改善例

# good_domain_service.py

class ApiRequestService:
    def execute(self, log: 'ApiRequestLog') -> None:
        print(log.request_id)

業務層は極力具体型表現優先が原則。ジェネリクスはAPI提供層や低層ライブラリに留めます。
レビューでは責務表現の抽象度を読みます。

良くない実装例: ケース4(型制約不足によるAPI誤用リスク)

# bad_missing_bound.py

from typing import TypeVar, List

T = TypeVar('T')

def get_first(items: List[T]) -> T:
    return items[0]
@Reviewer
itemsが空リストでも型安全が保証されていません。制約または安全性担保が必要です。

問題点

  • 空リスト時のIndexError設計漏れ
  • None返却型や安全性保証が型レベルで欠落
  • 呼び出し側で事故発生リスク高

改善例

# good_optional_return.py

from typing import TypeVar, List, Optional

T = TypeVar('T')

def get_first(items: List[T]) -> Optional[T]:
    return items[0] if items else None

返却型Optional化で呼び出し側の安全性が型レベルで表現できます。TypeVar活用時もAPI契約を型で表現する設計力が重要です。

観点チェックリスト

まとめ

TypeVarとジェネリクス設計は、設計者の「抽象化バランス力」そのものを映す領域です。
レビューアーは

  • そもそもジェネリクスが必要か?
  • 制約設計は適切か?
  • 呼び出し側が事故らないか?

を常に読み解き、「汎用性は必要最小限、安全性は最大化」を徹底するレビュー習慣を身につけていきます。
ジェネリクスレビューは抽象化教育教材として現場育成で極めて有効です。