TypeScriptでクラスを使わないクリーンコード!継承の闇から抜け出し結合度を低く保つアプローチ

TypeScriptで継承を避けながらクリーンコードと疎結合設計を実現するイメージ プログラミング言語

TypeScriptで大規模なアプリケーションを開発していると、自然と「クラス設計」を中心に考え始める場面があります。
特にオブジェクト指向の経験があると、継承を使って共通化し、責務を整理し、再利用性を高めようとするのはごく自然な流れです。
しかし実際には、その継承構造がコードベース全体の柔軟性を奪い、変更に弱い設計を生み出してしまうケースは少なくありません。

たとえば、基底クラスの修正が予期しない副作用を引き起こしたり、派生クラスが増えすぎて依存関係を追えなくなったり、「このクラスはどこまで責務を持つべきか」が曖昧になる問題は、多くの開発現場で発生しています。
TypeScriptはJavaやC#のような伝統的オブジェクト指向言語とは異なり、構造的型付けや関数型的アプローチとの親和性が高い言語です。
そのため、必ずしもクラス中心で設計する必要はありません。

近年では、Reactをはじめとするモダンなエコシステムでも、クラスベース設計より「関数の組み合わせ」や「依存の注入」を軸にしたアプローチが主流になっています。
これは単なる流行ではなく、結合度を下げ、変更容易性を高め、テストしやすいコードを書くうえで合理的だからです。

この記事では、TypeScriptであえてクラスを使わずに設計するメリットを整理しながら、継承が抱える構造的な問題点、そして関数・オブジェクトリテラル・コンポジションを活用してクリーンなコードを実現する具体的な考え方を解説していきます。
単なる「クラス否定論」ではなく、保守性と拡張性を両立するために、どのように依存関係を整理すべきかを実践的な視点で掘り下げます。

TypeScriptでクラス設計が複雑化しやすい理由

複雑なクラス図と依存関係に悩むTypeScript開発者のイメージ

TypeScriptはクラス構文をサポートしているため、一見するとJavaやC#のようなオブジェクト指向言語と同じ感覚で設計できるように見えます。
実際、TypeScriptには継承、抽象クラス、アクセス修飾子など、多くのオブジェクト指向機能が備わっています。
そのため、大規模開発において「まずクラスを作る」という思考になりやすいのは自然な流れです。

しかし、TypeScriptは本質的にはJavaScriptの拡張であり、JavaScriptの柔軟性や関数型的特性を強く引き継いでいます。
この性質を無視して過度にクラス中心の設計へ寄せると、結果として結合度が高く、変更に弱いコードになりやすいのです。

特にフロントエンドやNode.js環境では、状態管理や非同期処理、依存注入、イベント駆動設計などが絡み合います。
その中で継承ベースの設計を多用すると、責務の境界が曖昧になり、コードの追跡コストが急激に増加します。

オブジェクト指向とTypeScriptの相性を改めて整理する

まず理解しておきたいのは、TypeScriptは「オブジェクト指向言語」ではなく、「JavaScriptに型システムを追加した言語」であるという点です。
ここを誤解すると、設計方針そのものがズレやすくなります。

JavaやC#では、クラスは設計の中心です。
インスタンス生成や型定義、責務分離の多くがクラスベースで行われます。
一方でTypeScriptは、関数、オブジェクトリテラル、クロージャなどを自然に組み合わせられる言語です。
つまり、クラスだけが設計手段ではありません。

たとえば、以下のような単純な処理を考えます。

type User = {
  id: string
  name: string
}
const formatUserName = (user: User): string => {
  return `${user.id}: ${user.name}`
}

これは非常にシンプルですが、十分に型安全であり、責務も明確です。
ここで無理にクラス化すると、逆に設計が肥大化するケースがあります。

class User {
  constructor(
    public id: string,
    public name: string
  ) {}
  formatName(): string {
    return `${this.id}: ${this.name}`
  }
}

この程度であれば問題ありません。
しかし実務では、このクラスにバリデーション、API通信、状態管理、ログ出力などが追加され始めます。
その結果、「Userクラス」が巨大化し、単一責任原則を破壊していきます。

特にTypeScriptでは構造的型付けが採用されています。
これは「同じ構造を持っていれば互換性がある」という考え方です。
そのため、クラス階層を作らなくても型安全な設計を実現できます。

比較項目 Java/C# TypeScript
型システム 名称的型付け 構造的型付け
設計中心 クラス 関数・オブジェクト
継承依存度 高い 必須ではない
柔軟性 比較的低い 高い

つまりTypeScriptでは、「クラスを使える」ことと、「クラス中心で設計すべき」は全く別の話なのです。

継承によって結合度が高くなる典型パターン

継承が問題化しやすい最大の理由は、親クラスと子クラスが強く結び付くことです。
これは単なるコード共有ではなく、「内部実装への依存」を生みます。

たとえば、以下のような構造はよく見かけます。

class BaseRepository {
  connectDatabase() {
    console.log("DB Connected")
  }
}
class UserRepository extends BaseRepository {
  findUser() {
    this.connectDatabase()
    return { id: 1, name: "Taro" }
  }
}

このコードは一見すると再利用性が高そうに見えます。
しかし、実際にはUserRepositoryBaseRepositoryの内部実装に依存しています。

もしconnectDatabase()の仕様変更が発生すると、すべての派生クラスへ影響が波及します。
さらに、継承階層が深くなると、以下のような問題が発生します。

  • どこでメソッドが定義されているのか追いにくい
  • 親クラス変更の影響範囲が読めない
  • 子クラスが親クラスの責務を引き継ぎすぎる
  • テスト時に不要な依存が混入する
  • 部分的な再利用が難しい

これはコンピューターサイエンスでいう「高結合」の典型例です。
モジュール同士の依存が強くなるほど、変更容易性は低下します。

特にTypeScriptでは、非同期処理や外部APIとの通信が頻繁に登場します。
そのため、継承ベースで共通処理を集約すると、副作用が親クラスへ集中しやすくなります。

結果として、以下のような「神クラス」が生まれがちです。

  • API通信を行う
  • ログを出力する
  • キャッシュを持つ
  • 状態を管理する
  • エラー処理を統括する

こうした設計は短期的には便利でも、長期的には保守性を著しく低下させます。

本来、TypeScriptは小さな関数や独立したモジュールを組み合わせる設計と非常に相性が良い言語です。
しかし継承を多用すると、その柔軟性を自ら捨てることになります。

そのため、モダンなTypeScript開発では「継承よりコンポジション」「クラスより関数」という方向へ設計思想がシフトしているのです。

継承ベースのTypeScriptコードが抱える保守性の問題

巨大な継承構造に苦しむ保守フェーズの開発現場イメージ

継承はオブジェクト指向における代表的な仕組みですが、TypeScriptの実務開発では保守性を悪化させる要因になることがあります。
特に中〜大規模プロジェクトでは、「共通化」のつもりで導入した継承構造が、結果として複雑な依存関係を生み出してしまうケースが少なくありません。

問題なのは、継承そのものではなく、「変更に対する影響範囲」が見えにくくなる点です。
TypeScriptはJavaScriptランタイム上で動作するため、動的な振る舞いと静的型システムが混在します。
その環境で継承を深く使うと、実行時の挙動追跡コストが急激に増加します。

さらに、フロントエンドやNode.js環境では、API通信、状態管理、非同期処理、イベント処理など、多数の副作用が発生します。
こうした副作用を基底クラスへ集約すると、責務の境界が曖昧になり、設計全体の可読性が低下していきます。

基底クラス修正による副作用が発生する理由

継承ベース設計が危険視される最大の理由は、「親クラスの変更が子クラス全体へ波及する」点にあります。
これはコンピューターサイエンスにおける高結合の典型例です。

たとえば、以下のような基底クラスを考えます。

class BaseService {
  protected log(message: string) {
    console.log(`[LOG]: ${message}`)
  }
}

この時点では問題ありません。
しかし実務では、次第に責務が追加されます。

class BaseService {
  protected log(message: string) {
    console.log(`[LOG]: ${message}`)
  }
  protected async authenticate() {
    return "token"
  }
  protected cache(data: unknown) {
    // cache logic
  }
}

すると、この基底クラスを継承しているすべてのクラスが、暗黙的にこれらの責務へ依存する状態になります。

class UserService extends BaseService {
  async getUser() {
    await this.authenticate()
    this.log("fetch user")
  }
}

この構造の問題は、「UserServiceが何に依存しているのか」がコード上で見えにくい点です。
authenticate()cache()がどのような副作用を持つのかを理解するためには、親クラスまで遡る必要があります。

さらに厄介なのは、基底クラスの仕様変更です。
たとえば認証処理を変更した場合、影響はすべての派生クラスへ波及します。

  • ログ出力形式が変わる
  • 認証フローが変わる
  • キャッシュ戦略が変わる
  • 非同期処理が追加される

このような変更が、関係ない子クラスの動作まで破壊することがあります。

特にTypeScriptでは、IDE補完や型推論が強力であるため、開発中は問題に気付きにくい傾向があります。
しかし設計レベルでは依存が密結合化しており、長期運用時に変更コストが急上昇します。

これは「継承によるコード再利用」が、実際には「実装共有による依存拡大」になっているからです。

TypeScriptでSOLID原則が崩れやすいケース

継承を多用したTypeScript設計では、SOLID原則が崩れやすくなります。
特に問題になりやすいのが、単一責任原則と依存関係逆転原則です。

単一責任原則は、「1つのモジュールは1つの責務だけを持つべき」という考え方です。
しかし継承ベース設計では、基底クラスへ共通処理を集約し続けるため、責務が肥大化しやすくなります。

たとえば、以下のような状況は実務で頻繁に発生します。

追加された責務 本来の責務 問題点
ログ出力 監視 ビジネスロジックと混在
API通信 データ取得 ネットワーク依存が増加
キャッシュ パフォーマンス改善 状態管理が複雑化
エラーハンドリング 障害対策 分岐が肥大化

この結果、基底クラスは「何でもできるクラス」になります。

さらに依存関係逆転原則も崩れやすくなります。
本来、高レベルモジュールは具体実装へ依存すべきではありません。
しかし継承構造では、子クラスが親クラスの実装詳細へ強く依存します。

つまり、「抽象への依存」ではなく、「実装への依存」が発生している状態です。

特にTypeScriptでは、関数を引数として渡せるため、本来は柔軟な依存注入が可能です。
にもかかわらず、継承へ依存すると設計が固定化されます。

たとえば、関数ベースなら以下のように責務分離できます。

type Logger = (message: string) => void
const fetchUser = async (
  logger: Logger
) => {
  logger("fetch user")
}

この設計では、ロガーの実装を自由に差し替えられます。
つまり依存方向が固定化されません。

一方、継承ベース設計では、親クラスの変更が構造全体へ波及します。
これはソフトウェア工学において、長期保守コストを増大させる典型的なパターンです。

そのため、モダンなTypeScript開発では、継承による共通化よりも「コンポジションによる組み合わせ」が重視されるようになっています。
小さな責務を独立させ、それらを関数として接続するほうが、結果的に保守しやすく、テストしやすいコードになりやすいのです。

なぜモダンTypeScript開発ではクラス離れが進んでいるのか

関数中心設計へ移行するモダンTypeScript開発のイメージ

近年のTypeScript開発では、以前ほどクラス中心の設計が採用されなくなっています。
これは単なるトレンドではなく、フロントエンドとバックエンド双方のアーキテクチャ変化によって、関数ベース設計のほうが合理的になってきたためです。

特にReactの普及は、この流れを大きく加速させました。
かつてReactではクラスコンポーネントが主流でしたが、Hooks導入以降は関数コンポーネントが標準となりました。
これは単なる書き方の変化ではなく、「状態と振る舞いを小さく分離する」という設計思想の変化でもあります。

さらにNode.js環境でも、ExpressやFastifyのような軽量フレームワークが一般化し、継承ベースより「関数を組み合わせる設計」が主流になっています。

TypeScriptは静的型付けを持ちながら、JavaScript由来の柔軟性も維持している言語です。
そのため、従来型オブジェクト指向よりも、関数型的な設計との相性が非常に良いのです。

ReactやNode.jsで広がる関数型アプローチ

モダンなTypeScript開発で関数型アプローチが支持される理由の一つは、「状態と副作用を分離しやすい」ことにあります。

クラスベース設計では、状態と振る舞いが同じインスタンスへ集約されます。
その結果、どこで状態が変更されるのか追跡しにくくなります。

一方、関数中心の設計では、入力と出力を明確に分離できます。

たとえば、Reactの関数コンポーネントは非常に典型的です。

type UserProps = {
  name: string
}
export const UserCard = ({
  name
}: UserProps) => {
  return <div>{name}</div>
}

このコードでは、副作用や内部状態が存在しません。
入力が決まれば出力も決まるため、挙動を予測しやすくなります。

さらにHooksを使う場合でも、状態管理を局所化できます。

const useCounter = () => {
  const [count, setCount] = useState(0)
  const increment = () => {
    setCount(count + 1)
  }
  return {
    count,
    increment
  }
}

ここで重要なのは、「責務単位で機能を分離できる」点です。

クラスベース設計では、状態管理、イベント処理、副作用処理が単一クラスへ集中しやすくなります。
しかし関数型アプローチでは、小さな関数へ責務を分割できます。

Node.jsでも同様です。

たとえばExpressでは、ミドルウェアを関数として接続します。

app.use(async (req, res, next) => {
  console.log(req.path)
  next()
})

この構造は継承を必要としません。
必要なのは「契約を満たす関数」であり、クラス階層ではないのです。

この設計には以下のメリットがあります。

  • 部分的な再利用がしやすい
  • テスト対象を小さく保てる
  • 依存関係を明示しやすい
  • 副作用を局所化できる
  • モジュール単位で交換しやすい

これはコンピューターサイエンスにおける「疎結合設計」の考え方そのものです。

特にJavaScriptランタイムは非同期処理が多いため、状態を持つクラスより、データフロー中心の設計のほうが管理しやすい傾向があります。

構造的型付けがTypeScriptにもたらした設計自由度

TypeScriptがクラス中心設計から離れやすい最大の理由は、「構造的型付け」を採用している点です。

これはJavaやC#のような名称的型付けとは根本的に異なります。

名称的型付けでは、「どのクラスから生成されたか」が重要です。
一方、TypeScriptでは「どのような構造を持っているか」が重要になります。

たとえば、以下のコードを見てみます。

type Point = {
  x: number
  y: number
}
const printPoint = (
  point: Point
) => {
  console.log(point.x, point.y)
}

この場合、Point型を明示的に継承する必要はありません。

const position = {
  x: 10,
  y: 20,
  z: 30
}
printPoint(position)

xyを持っていれば、型として成立します。

これは非常に重要な特徴です。
つまりTypeScriptでは、「共通の親クラス」を作らなくても、型安全なコードを書けるのです。

従来型オブジェクト指向では、共通インターフェースを作るために継承構造が必要になるケースが多くありました。
しかしTypeScriptでは、構造そのものが契約になります。

比較項目 名称的型付け 構造的型付け
型互換性 クラス名で判断 構造で判断
継承依存 高い 低い
柔軟性 制限されやすい 高い
再利用性 階層ベース 組み合わせベース

この特性によって、TypeScriptでは「コンポジション中心設計」が成立しやすくなっています。

つまり、必要な機能を小さな単位へ分割し、それらを関数やオブジェクトとして組み合わせる設計が自然になるのです。

さらに構造的型付けは、APIレスポンスやJSONデータとの相性も非常に良好です。
Web開発ではオブジェクト構造が頻繁に変化するため、クラス階層より柔軟な型システムのほうが実用的なのです。

その結果、モダンTypeScript開発では「継承による抽象化」より、「型による契約」と「関数による組み合わせ」が重視されるようになっています。

TypeScriptでクラスを使わずにクリーンコードを書く基本戦略

シンプルな関数設計で整理されたTypeScriptコードのイメージ

TypeScriptで保守性の高いコードを書く場合、重要なのは「どの構文を使うか」ではなく、「依存関係をどう整理するか」です。
クラスを完全に禁止する必要はありません。
しかし、設計の中心を継承へ置くと、変更に弱く、責務が肥大化したコードになりやすくなります。

モダンTypeScript開発で重視されているのは、小さな責務を独立した関数やモジュールとして分離し、それらを必要に応じて組み合わせるアプローチです。
これはソフトウェア工学における「疎結合・高凝集」の考え方に近いものです。

特にJavaScriptランタイムは動的性質が強く、状態管理や非同期処理が複雑化しやすいため、継承ベースよりコンポジション中心のほうが設計を単純化しやすい傾向があります。

TypeScriptは構造的型付けを採用しているため、クラス階層を作らなくても型安全な設計を実現できます。
そのため、関数・オブジェクト・依存注入を軸にした設計が非常に有効です。

関数とオブジェクトリテラルで責務を分割する

クリーンコードを書くうえで最も重要なのは、「責務の境界を小さく保つこと」です。

継承ベース設計では、複数の責務がクラスへ集約されやすくなります。
しかし関数中心設計では、処理単位で責務を分離できます。

たとえば、ユーザー情報を取得して加工する処理を考えます。

type User = {
  id: string
  name: string
}
const fetchUser = async (
  id: string
): Promise<User> => {
  return {
    id,
    name: "Taro"
  }
}
const formatUser = (
  user: User
): string => {
  return `${user.id}: ${user.name}`
}

この構造では、データ取得と表示加工が完全に分離されています。

さらに、責務ごとに関数を独立させることで、変更範囲を局所化できます。

  • API仕様変更はfetchUser
  • 表示変更はformatUser
  • 型変更はUser

このように影響範囲が明確になります。

また、オブジェクトリテラルを使えば、必要な振る舞いだけを持つモジュールを構築できます。

const logger = {
  info(message: string) {
    console.log(message)
  },
  error(message: string) {
    console.error(message)
  }
}

この設計では、クラスのような継承構造が存在しません。
そのため、依存関係を追跡しやすくなります。

TypeScriptでは「構造」が型になるため、必ずしもクラスで抽象化する必要がありません。
これはJava系オブジェクト指向とは大きく異なる特徴です。

コンポジションで機能を組み合わせる設計手法

モダンTypeScript設計で重要視されているのが「コンポジション」です。

コンポジションとは、小さな機能を組み合わせて全体を構築する考え方です。
継承のように「親子関係」を作るのではなく、「部品を接続する」イメージに近い設計です。

たとえば、認証処理とログ出力を組み合わせたい場合を考えます。

const createLogger = () => {
  return {
    log(message: string) {
      console.log(message)
    }
  }
}
const createAuth = () => {
  return {
    authenticate(token: string) {
      return token === "valid-token"
    }
  }
}

これらを組み合わせます。

const createUserService = () => {
  const logger = createLogger()
  const auth = createAuth()
  return {
    login(token: string) {
      logger.log("login attempt")
      return auth.authenticate(token)
    }
  }
}

ここでは継承が一切存在しません。

しかし、

  • ログ機能
  • 認証機能
  • ユーザーサービス

が独立した責務として成立しています。

この構造の利点は、「必要な機能だけを組み合わせられる」点です。

継承ベース設計では、親クラスの責務を丸ごと引き継ぐことになります。
しかしコンポジションでは、必要な機能だけ選択できます。

設計手法 特徴 問題点
継承 親子関係で共通化 密結合になりやすい
コンポジション 機能を組み合わせる 責務分離が必要
関数分割 処理単位で独立 設計整理が必要

TypeScriptは関数を第一級オブジェクトとして扱えるため、コンポジションとの相性が非常に良好です。

特にReact Hooksの設計思想は、このコンポジションモデルそのものです。

依存性注入を使って結合度を下げる考え方

疎結合設計を実現するうえで重要なのが、依存性注入です。

依存性注入とは、「必要な機能を外部から渡す」考え方です。
これによって、モジュール同士の直接依存を避けられます。

たとえば、ログ出力へ直接依存するコードを考えます。

const saveUser = async (
  name: string
) => {
  console.log(name)
}

この場合、saveUserconsole.logへ直接依存しています。

しかし依存を外部化すると、設計が柔軟になります。

type Logger = (
  message: string
) => void
const saveUser = async (
  name: string,
  logger: Logger
) => {
  logger(name)
}

この構造では、ログ出力方法を自由に変更できます。

saveUser("Taro", console.log)

あるいは、

saveUser("Taro", (msg) => {
  fileWriter.write(msg)
})

のような差し替えも可能です。

これは依存方向を逆転させている状態です。
つまり、具体実装ではなく「契約」に依存しています。

この設計のメリットは非常に大きく、

  • テストしやすい
  • モックを注入しやすい
  • 機能差し替えが容易
  • 副作用を局所化できる
  • 再利用性が高まる

といった恩恵があります。

TypeScriptでは型エイリアスやインターフェースを使うことで、この契約を簡潔に表現できます。
そのため、重いDIコンテナや継承構造を使わなくても、十分に柔軟なアーキテクチャを構築できます。

結果として、モダンTypeScript開発では「継承による抽象化」より、「関数による責務分離」と「依存注入による疎結合化」が重視されるようになっているのです。

TypeScriptの関数型設計でテストしやすいコードを実現する

テストコードとTypeScript関数設計を並べた開発環境イメージ

TypeScriptで保守性の高いシステムを構築するうえで、テスト容易性は極めて重要な要素です。
どれだけ設計が美しく見えても、変更時に安全性を担保できなければ、長期運用では破綻しやすくなります。

特に大規模開発では、機能追加よりも「既存コードを壊さず変更する」コストのほうが大きくなります。
そのため、変更耐性を高める設計が必要になります。

ここで重要になるのが、関数型設計の考え方です。

TypeScriptにおける関数型設計は、純粋関数を徹底することだけを意味しません。
むしろ実務では、「副作用を分離し、責務を小さく保つ」という思想のほうが重要です。

継承ベース設計では、状態や外部依存がクラス内部へ集約されやすく、テスト対象が肥大化します。
一方、関数中心設計では、入力と出力を明示しやすいため、テスト単位を小さく保てます。

この違いが、テストコードの保守性に大きな差を生みます。

副作用を分離するとユニットテストが簡単になる

テストを難しくする最大の要因は、「副作用」です。

副作用とは、関数外部の状態へ影響を与える処理を指します。

代表的な副作用には以下があります。

  • API通信
  • データベースアクセス
  • ファイル操作
  • ログ出力
  • グローバル状態変更
  • 時刻取得

これらがビジネスロジックと密結合すると、テストが急激に複雑化します。

たとえば、以下のようなコードを考えます。

const createUser = async (
  name: string
) => {
  const response = await fetch("/users", {
    method: "POST",
    body: JSON.stringify({ name })
  })
  console.log("user created")
  return response.json()
}

この関数は、

  • API通信
  • JSON処理
  • ログ出力

を同時に行っています。

この状態では、ユニットテスト時にネットワーク依存が発生します。
さらにログ処理も含まれるため、テスト対象の責務が曖昧になります。

そこで重要なのが、副作用の分離です。

type User = {
  name: string
}
const buildUserPayload = (
  name: string
): User => {
  return { name }
}

副作用を持たない部分を切り出します。

const saveUser = async (
  payload: User
) => {
  return fetch("/users", {
    method: "POST",
    body: JSON.stringify(payload)
  })
}

この設計では、

  • データ生成
  • API通信

が分離されています。

その結果、純粋なロジック部分は簡単にテストできます。

describe("buildUserPayload", () => {
  it("returns valid payload", () => {
    expect(
      buildUserPayload("Taro")
    ).toEqual({
      name: "Taro"
    })
  })
})

ここではネットワークもモックも不要です。

このように、関数型設計では「副作用を境界へ押し出す」ことで、テスト対象を小さく保てます。

これはコンピューターサイエンスでいう「関心の分離」に近い考え方です。

モック依存を減らせる設計のメリット

継承ベース設計では、テスト時に大量のモックが必要になりやすい傾向があります。

たとえば、クラス内部で複数の依存を生成している場合を考えます。

class PaymentService {
  async process() {
    const db = new Database()
    const logger = new Logger()
    logger.log("start")
    await db.save()
  }
}

このコードをテストするには、

  • Database
  • Logger

のモックが必要になります。

さらに、内部生成されているため、依存差し替えが困難です。

結果として、

  • モック設定が肥大化する
  • テストコードが実装依存になる
  • リファクタリング耐性が低下する

といった問題が発生します。

一方、関数ベース設計では依存を外部注入できます。

type Database = {
  save(): Promise<void>
}
type Logger = {
  log(message: string): void
}
type Dependencies = {
  db: Database
  logger: Logger
}

依存を受け取る形に変更します。

const createPaymentService = (
  deps: Dependencies
) => {
  return {
    async process() {
      deps.logger.log("start")
      await deps.db.save()
    }
  }
}

この構造では、テスト時に必要最小限のモックだけを渡せます。

const mockDb = {
  save: async () => {}
}
const mockLogger = {
  log: () => {}
}

これだけでテスト可能です。

このアプローチの重要な点は、「実装詳細ではなく契約へ依存している」ことです。

つまり、

  • saveが存在する
  • logが存在する

という構造だけ保証されれば成立します。

TypeScriptの構造的型付けは、この設計と非常に相性が良いのです。

さらにモック依存が減ると、テストコード自体の保守性も向上します。

設計スタイル テスト特徴 問題点
継承ベース 内部依存が多い モック肥大化
クラス集中型 状態共有が多い テスト分離困難
関数型設計 入出力が明確 小さな単位で検証可能

実務では、「テストしやすいコード」は「責務が整理されたコード」であるケースがほとんどです。

そのため、モダンTypeScript開発では、テスト容易性そのものを設計品質の指標として扱うことが増えています。

継承による抽象化ではなく、副作用分離・依存注入・関数分割を軸にした設計のほうが、結果的に長期保守へ強いコードになりやすいのです。

TypeScriptで実践するクラスを使わないアーキテクチャ例

関数ベースのTypeScriptアーキテクチャ構成図イメージ

ここまで見てきたように、TypeScriptでは継承ベース設計よりも、関数とコンポジションを中心にした設計のほうが保守性を高めやすい傾向があります。
しかし実際の開発現場では、「理論は理解できても、どうアーキテクチャへ落とし込むべきか」が難しいポイントになります。

重要なのは、「クラスを使わないこと」自体を目的にしないことです。
本質は、依存関係を整理し、責務を局所化し、変更容易性を高めることにあります。

そのため、実務では以下のような考え方が重要になります。

  • 状態と副作用を分離する
  • 機能単位でモジュール化する
  • 依存を外部注入する
  • 小さな関数を組み合わせる
  • データフローを明確にする

これらを意識すると、自然に「クラスを大量に使わない設計」へ近づいていきます。

特にフロントエンドとバックエンドでは、それぞれ異なる形でこのアプローチが活用されています。

フロントエンドでの状態管理と関数分離

モダンフロントエンド開発では、Reactを中心に「関数ベース設計」が主流になっています。

以前のReactではクラスコンポーネントが一般的でした。
しかしHooks導入以降は、状態管理や副作用処理を関数として分離できるようになりました。

たとえば、ユーザー情報取得処理を考えます。

type User = {
  id: string
  name: string
}

まず、APIアクセスを独立関数にします。

const fetchUser = async (
  id: string
): Promise<User> => {
  const response = await fetch(`/users/${id}`)
  return response.json()
}

次に状態管理をHooksへ切り出します。

const useUser = (id: string) => {
  const [user, setUser] =
    useState<User | null>(null)
  useEffect(() => {
    fetchUser(id).then(setUser)
  }, [id])
  return user
}

UI側は状態取得だけを担当します。

const UserProfile = ({
  id
}: {
  id: string
}) => {
  const user = useUser(id)
  if (!user) {
    return <div>Loading...</div>
  }
  return <div>{user.name}</div>
}

この構造では、

  • API通信
  • 状態管理
  • UI描画

が明確に分離されています。

継承ベース設計では、これらが単一クラスへ集中しやすくなります。
しかし関数分離を徹底すると、責務境界が非常に明確になります。

さらにReact Hooksは「コンポジション」によって機能を組み合わせられます。

const useLogger = () => {
  return (message: string) => {
    console.log(message)
  }
}
const useTracking = () => {
  return (event: string) => {
    analytics.track(event)
  }
}

これらを必要な場所だけで組み合わせます。

この設計の重要な点は、「継承ではなく組み合わせ」で機能拡張していることです。

結果として、

  • 変更範囲が限定される
  • テスト単位が小さくなる
  • 再利用しやすい
  • UIロジックを分離できる

といったメリットが生まれます。

これはReactだけでなく、VueやSolidJSなどのモダンフロントエンド全体に共通する設計思想です。

バックエンドAPI設計でクラスを減らす方法

バックエンド開発でも、近年はクラス中心設計から離れる流れが強くなっています。

特にNode.js系フレームワークでは、関数ベースアーキテクチャとの相性が非常に良好です。

たとえば、典型的なクラスベース設計では、以下のような構造になりがちです。

class UserService {
  async findUser(id: string) {
    // DB access
  }
}

一見シンプルですが、実務ではこのクラスへ、

  • DBアクセス
  • キャッシュ
  • ログ出力
  • 認証
  • バリデーション

などが集約され始めます。

その結果、巨大サービスクラスが生まれます。

一方、関数中心設計では責務ごとに分離します。

type User = {
  id: string
  name: string
}

DBアクセスを独立させます。

const findUserById = async (
  id: string
): Promise<User> => {
  return db.user.find(id)
}

バリデーションも分離します。

const validateUserId = (
  id: string
) => {
  return id.length > 0
}

APIハンドラは組み合わせるだけです。

const getUserHandler = async (
  req,
  res
) => {
  const id = req.params.id
  if (!validateUserId(id)) {
    return res.status(400).send()
  }
  const user =
    await findUserById(id)
  return res.json(user)
}

この構造では、各モジュールが単一責務を持っています。

また、関数単位で依存を差し替えられるため、テスト容易性も高くなります。

設計スタイル 特徴 長期保守性
巨大サービスクラス 責務集中 低い
継承ベース 依存共有 低下しやすい
関数分割型 責務独立 高い
コンポジション型 柔軟な組み合わせ 高い

特にTypeScriptでは構造的型付けがあるため、インターフェース継承へ強く依存しなくても型安全性を維持できます。

さらに最近のNode.js開発では、FastifyやHonoのような軽量フレームワークが普及しており、「小さな関数を接続する」設計が標準化しつつあります。

つまり、モダンTypeScript開発では、クラスを中心に据えるよりも、「責務単位の関数」と「依存注入可能なモジュール」を組み合わせるほうが、実務に適したアーキテクチャになりやすいのです。

VSCodeやESLintを活用してTypeScriptの設計品質を保つ

VSCodeとESLintでTypeScriptコードを改善するイメージ

TypeScriptでクラスを使わない設計や関数中心のアーキテクチャを採用したとしても、それだけで自動的にコード品質が保証されるわけではありません。
むしろ設計の自由度が高い分、チーム開発では「どのようなルールで統一するか」が重要になります。

ここで中心的な役割を果たすのが、VSCodeとESLintです。
これらは単なる補助ツールではなく、設計思想をコードベースへ強制するための仕組みとして機能します。

特に関数分割やコンポジションを重視する設計では、命名規則・副作用の管理・依存関係の制御が曖昧になると、すぐにスパゲッティ化します。
そのため静的解析ツールによる制約は、設計品質を維持するうえで不可欠です。

またVSCodeはTypeScriptとの統合度が非常に高く、型情報をリアルタイムに参照しながらリファクタリングを行えるため、設計の試行錯誤コストを大幅に下げます。

型安全性を高めるLint設定のポイント

ESLintは単なるコードスタイルチェックツールではなく、設計ルールの強制装置として利用することが重要です。
特にTypeScriptでは型情報と連携したルールを設定することで、潜在的な設計ミスを早期に検出できます。

例えば、以下のような観点が重要になります。

  • any型の禁止
  • 未使用変数の検出
  • 明示的な戻り値型の強制
  • 副作用関数の分離ルール
  • import順序の統一

これらを適切に設定することで、関数分割やコンポジション設計の破綻を防ぐことができます。

たとえば、型安全性を重視した場合のESLintルールは以下のように構成されます。

// @typescript-eslint/no-explicit-any を有効化
// 明示的な型定義を強制する

このルールは単純に見えますが、実務では非常に効果が大きく、設計段階での曖昧さを排除できます。

また、関数中心設計では「副作用の位置」を明確にすることが重要です。
そのため、ESLintのルールで副作用を持つ処理を分離することが推奨されます。

ルールカテゴリ 目的 効果
型制約 any排除 型安全性向上
副作用管理 関数分離 テスト容易性向上
命名規則 一貫性確保 可読性向上
import制御 依存整理 構造明確化

これらのルールは単なるスタイルではなく、設計そのものを制御する役割を持ちます。

大規模開発で役立つTypeScript向けツール

大規模なTypeScriptプロジェクトでは、ESLintやVSCodeだけでは不十分になる場合があります。
そのため、補助的なツール群を組み合わせることで、設計品質をさらに安定させることができます。

まず重要なのがTypeScript Compilerそのものの厳格設定です。

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

この設定により、曖昧な型推論を排除し、設計上の意図を明確にコードへ反映できます。

さらに大規模開発では以下のようなツールが役立ちます。

  • Prettier:コードフォーマット統一
  • Husky:コミット前フックで品質保証
  • lint-staged:変更ファイルのみLint適用
  • ts-prune:未使用エクスポート検出
  • TypeDoc:APIドキュメント自動生成

これらを組み合わせることで、設計と実装の乖離を防ぐことができます。

特に重要なのは「人間の判断に依存しない仕組み化」です。
クラス設計のような構造的複雑性は、レビューだけでは防ぎきれません。
そのためツールによる強制力が必要になります。

VSCodeも単なるエディタではなく、TypeScript開発においては設計補助ツールとして機能します。
リファクタリング機能、型参照ジャンプ、インターフェース追跡などは、クラスを使わない設計において特に有効です。

結果として、これらのツールを適切に組み合わせることで、以下のような状態を実現できます。

  • 設計ルールの自動強制
  • 変更影響範囲の可視化
  • 型安全性の担保
  • 副作用の局所化
  • チーム間の認識統一

つまり、モダンTypeScript開発では「コードを書く前にルールで設計を固める」ことが重要になります。
クラスを中心に据えるのではなく、ツールと型システムを活用して、関数ベース設計を安定的に運用することが実務的な解になります。

TypeScriptで継承を避けながら拡張性を高める設計へ

疎結合で拡張しやすいTypeScript設計を表現したイメージ

TypeScriptにおける設計の最終的な焦点は、「いかに変更に強く、かつ拡張しやすい構造を維持するか」という点に集約されます。
従来のオブジェクト指向設計では継承を用いて機能拡張を行うことが一般的でしたが、現代のTypeScript開発ではその前提自体が再評価されています。
理由は明確で、継承は一見すると再利用性を高めるように見えますが、実際には依存関係を固定化し、変更コストを増大させる傾向があるためです。

特に中規模以上のプロジェクトでは、基底クラスを中心にした設計が徐々に「見えない依存」を増やしていきます。
子クラスは親クラスの実装詳細に引きずられやすくなり、結果として局所的な変更が全体へ波及する構造が形成されます。
これは長期運用において典型的な技術的負債の原因になります。

そのため現代のTypeScriptでは、継承ではなくコンポジションを中心にした設計へ移行することが重要になります。
コンポジションは機能を「継承する」のではなく「組み合わせる」ため、各モジュールの独立性が高まり、変更の影響範囲を局所化できます。

さらにTypeScriptは構造的型付けを採用しているため、クラス階層を作らなくても型安全性を維持できます。
この特性が、継承を必要としない設計を強く後押ししています。

拡張性を高めるための基本戦略は以下のように整理できます。

  • 機能を小さな関数単位へ分割する
  • 依存を外部から注入する
  • 状態と副作用を分離する
  • モジュールを純粋に保つ
  • コンポジションで機能を組み立てる

これらの原則は個別に見ると単純ですが、組み合わせることで強力な設計基盤を形成します。

特に重要なのは「変更容易性」と「再利用性」を同時に成立させる点です。
継承ではこの2つがトレードオフになりやすいのに対し、コンポジションベースの設計では両立が可能になります。

たとえば、認証・ログ・データ取得といった横断的な機能を考えた場合でも、クラス継承を使わずに以下のように分離できます。

type Logger = (message: string) => void
const createLogger = (): Logger => {
  return (message) => {
    console.log(message)
  }
}
type Auth = {
  verify(token: string): boolean
}
const createAuth = (): Auth => {
  return {
    verify(token: string) {
      return token === "valid"
    }
  }
}

これらを組み合わせることで、サービス層は非常にシンプルになります。

const createService = (
  logger: Logger,
  auth: Auth
) => {
  return {
    execute(token: string) {
      if (!auth.verify(token)) {
        logger("auth failed")
        return
      }
      logger("success")
    }
  }
}

この設計では、各機能が完全に独立しているため、差し替えやテストが容易になります。
また、継承構造が存在しないため、変更の影響範囲を予測しやすいという利点もあります。

さらに重要なのは、TypeScriptの型システムがこの設計と非常に相性が良い点です。
構造的型付けにより、「どのクラスか」ではなく「どの形を持っているか」で互換性が決まるため、抽象化のために継承階層を構築する必要がありません。

この結果として、拡張性は以下のような形で実現されます。

  • 新機能は既存コードを変更せず追加できる
  • 既存機能は独立して保守可能
  • テスト対象が小さく保たれる
  • 依存関係が明示的になる
  • チーム開発で衝突が起きにくい

また、実務的な観点では、継承を避けることでオンボーディングコストも低下します。
複雑なクラス階層を理解する必要がなくなり、関数単位でコードを追えるため、コードベース全体の可読性が向上します。

結果として、現代のTypeScript設計における最適解は「クラスを排除すること」ではなく、「継承依存を排除し、関数とコンポジションで構成すること」にあります。
これにより、拡張性と保守性を両立した柔軟なアーキテクチャを実現できるのです。

コメント

タイトルとURLをコピーしました