【実務・中級編】Interfaceのメソッド定義:関数型プログラミングとオブジェクト指向の境界線 – TypeScript コア・型システムの基礎解析バイブル

フロントエンドのコードレビューをしていて、最も頻繁に遭遇する設計の迷いどころがここだ。

// パターンA: メソッド定義
interface UserA {
getName(): string;
}

// パターンB: 関数プロパティ定義
interface UserB {
getName: () => string;
}

「どっちも同じだろ? 好きに書けばいいじゃん」と思ったそこのあなた。その認識のままだと、大規模なコンポーネント設計や非同期API連携のレイヤーで、TypeScriptの型システムが持つ「真の強力な機能」をドブに捨てることになる。

今回は、TypeScriptのコア・型システムの深淵を知るチーフアーキテクトの視点から、この2つの書き方の決定的な違い——「オブジェクト指向の作法」と「関数型プログラミングの厳密性」の境界線——をロジカルに解説しよう。

—

1. コンパイラは何を見ているのか?(型評価の裏側)

まず結論から言おう。この2つは、型チェッカーの挙動において全く別物である。

パターンA:メソッド定義(Method Signature)

オブジェクト指向的なアプローチだ。TypeScriptのコンパイラはこの構文を見たとき、特別な挙動をする。
最も重要な特徴は、メソッドにおける `this` の共変性(Covariance)が無視され、関数型が常に「Bivariant(双変)」として扱われるという点だ(厳密には `strictFunctionTypes` が有効な場合でも、メソッド構文は例外的に双変になる)。

さらに、メソッド定義は 「Bugs me silently(静かにバグを生む)」 特性を持っている。
オブジェクトの拡張や継承、そして「構造的型付け(Structural Typing)」において、意図しない代入やオーバーライドを許容してしまう緩さがあるのだ。

パターンB:関数型プログラミング(Property with Function Type)

関数型的なアプローチだ。プロパティに対して厳密な関数型(Arrow Function syntax)をバインドしている。
こちらは `strictFunctionTypes` の恩恵を100%受け、引数の型は反変(Contravariant)として厳密に評価される。つまり、より広範な引数を受け取る関数を安全に代入できるようになり、型安全性のメッシュが非常に細かくなる。

—

2. 実務で起きる「致命的なバグ」の正体

なぜ実務でパターンA(メソッド定義)を多用してはいけないのか。
非同期APIクライアントや、Reactなどのコンポーネント設計を例に取ろう。

罠:コールバックの代入と `this` の迷子

interface ApiClient {
// パターンA: メソッド定義
fetchDataA(id: string): Promise;

// パターンB: 関数型プロパティ
fetchDataB: (id: string) => Promise;
}

一見、何が違うのかわからない。しかし、この `ApiClient` を実装し、関数を別の変数に抽出(Destructuring / Extraction)して渡すコードを書いた瞬間、地獄が始まる。

const client: ApiClient = {
fetchDataA: async (id) => { / … / },
fetchDataB: async (id) => { / … / },
};

// 関数として切り出す
const { fetchDataA, fetchDataB } = client;

// パターンAのメソッド定義は、呼び出し元で `this` のコンテキストが崩壊した際に
// 実行時エラー(TypeError: Cannot read properties of undefined (reading ‘…’))を引き起こす温床になる。
// 一方、パターンBの関数プロパティは、アロー関数としてレキシカルに束縛されるため、
// どこに抽出されようとも `this` の迷子が発生しない。

関数型プログラミング(FP)のパラダイムでは、関数は「ファーストクラス・シチズン(第一級オブジェクト)」である。独立した純粋関数として扱われるべきであり、オブジェクトの内部ステート(`this`)に暗黙的に依存するメソッド定義は、現代のコンポーネント設計やイミュータブルな状態管理においてノイズでしかない。

—

3. プロダクションコードで示す「正しい設計」

では、フロントエンドのアーキテクチャ(API連携、状態管理、コンポーネントProps)において、どのように使い分けるべきか。

答えはシンプルだ。「特別な理由がない限り、すべて関数プロパティ(パターンB)で書け」。

以下に、実務の現場ですぐに応用可能な、極めて堅牢なプロダクションコードの例を示す。

// ==========================================
// Domain: 非同期API連携とイベントハンドリングの設計
// ==========================================

export type ID = string & { readonly __brand: unique symbol };

export interface UserEntity {
readonly id: ID;
readonly name: string;
}

/

  • Repository Interface
  • 全ての操作を関数プロパティで定義し、DI(依存性注入)やモック化を完全に予測可能にする。

/
IUserRepository = interface {
// ✖ 悪い例 (メソッド定義): 継承時に型チェックが緩くなり、thisのバグを誘発する
// findById(id: ID): Promise;

// ⭕ 良い例 (関数プロパティ): 厳密な反変性を保ち、純粋関数として扱える
readonly findById: (id: ID) => Promise;
readonly save: (user: UserEntity) => Promise;
}

/

  • 実際のプロダクション実装

/
export const createSqlUserRepository = (dbPool: DatabasePool): IUserRepository => ({
findById: async (id) => {
const row = await dbPool.query(‘SELECT FROM users WHERE id = ?’, [id]);
if (!row) throw new Error(`User not found: ${id}`);
return { id: row.id as ID, name: row.name };
},

save: async (user) => {
await dbPool.query(‘REPLACE INTO users (id, name) VALUES (?, ?)’, [user.id, user.name]);
}
});

なぜ `readonly` をつけるのか?

関数プロパティにするだけではなく、`readonly` 修飾子を付与することに注目してほしい。
これにより、インスタンス化された後に外部から関数が書き換えられる(モンキーパッチや不意のオーバーライド)のをコンパイル時に完全に防ぐことができる。
オブジェクト指向の「ポリモーフィズム」を保ちつつ、関数型の「イミュータビリティ(不変性)」を強制する——これこそが、TypeScriptを極めたアーキテクトの技法だ。

—

4. チーフアーキテクトからの提言:使い分けの基準

レビューでメンバーから「どっちで書くべきですか?」と聞かれたら、私はこう答えている。

1. 基本方針:すべて関数プロパティ(`prop: (args) => void`)で書く。

  • Reduxのディスパッチ、Reactのイベントハンドラー、APIクライアント、ユーティリティなど、現代のWebフロントエンドで扱う関数の99%はこれに該当する。
  • `strictFunctionTypes` の恩恵を受けられ、構造的型付けにおいて意図しない部分型関係(subtyping)を防げるため圧倒的にバグが少ない。

2. 例外:クラスのモデリングや、オーバーロード(Overloads)が必要な場合のみメソッド定義を使う。

  • クラスの `implements` 用のインターフェースで、かつメソッドのオーバーロードシグネチャを複数定義したい場合に限り、メソッド構文の特権(オーバーロードの簡潔な記述)を利用する。

// オーバーロードが必要な稀なケース
interface Logger {
log(message: string): void;
log(message: string, context: Record): void;
}

まとめ

「Interfaceのメソッド定義」と「関数型プログラメントのプロパティ定義」の選択は、単なるコーディングスタイルの好みではない。それはコンパイラの型評価の厳密さをコントロールし、ランタイムエラーの温床を断つための重要なアーキテクチャ上の決定である。

明日からのコードレビューでは、チームメンバーが書いたインターフェースのコロン(`:`)とアロー(`=>`)に目を光らせてほしい。その小さな記号の選択が、あなたのプロダクトを堅牢なものにするか、あるいは脆弱なものにするかの分かれ道なのだから。

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