こんにちは!TypeScriptの世界へようこそ。フルスタックアーキテクトの私です。
今回は、TypeScriptを学ぶ誰もが一度は手を止める「Interface(インターフェース)のメソッド定義」について、深掘りしていきましょう。
Interfaceの中にメソッドを書くとき、次の2つの書き方があるのを見たことありませんか?
// パターンA:メソッド構文(Method Signature)
interface GreeterA {
greet(name: string): string;
}
// パターンB:プロパティ構文(関数型プロパティ)
interface GreeterB {
greet: (name: string) => string;
}
「あれ、これって何が違うの? どっちを使っても同じじゃないの?」と思ったそこのあなた。実はここ、オブジェクト指向の世界と関数型プログラミングの世界が交差する、TypeScriptの非常に深くて面白い境界線なんです。
ここをクリアすれば、あなたのTypeScriptの型設計スキルは一段と洗練されますよ。一緒に優しく紐解いていきましょう!
—
1. 基本的な使い方と見た目の違い
まずは、それぞれの基本的な使い方を確認しておきましょう。
オブジェクト指向的なアプローチ:メソッド構文(パターンA)
JavaやC#などのオブジェクト指向言語出身者にはお馴染みの書き方です。「このオブジェクトはこういう振る舞い(メソッド)を持っていますよ」と宣言します。
interface UserWithMethod {
name: string;
// メソッド構文
sayHello(message: string): void;
}
const alice: UserWithMethod = {
name: “Alice”,
sayHello(message: string) {
console.log(`${this.name} says: ${message}`);
},
};
alice.sayHello(“こんにちは!”);
// 実行結果: Alice says: こんにちは!
関数型プログラミング的なアプローチ:プロパティ構文(パターンB)
こちらは、「オブジェクトの中に、値として関数が格納されている」という考え方です。JavaScriptやTypeScriptの本質を突いたアプローチですね。
interface UserWithProperty {
name: string;
// プロパティ構文(アロー関数型の割り当て)
sayHello: (message: string) => void;
}
const bob: UserWithProperty = {
name: “Bob”,
// アロー関数や無名関数を代入する
sayHello: (message: string) => {
// 注意:ここでの this は外側のスコープを向くか、型エラーの元になることがあります
console.log(`Hello: ${message}`);
},
};
bob.sayHello(“Hi!”);
// 実行結果: Hello: Hi!
一見すると「書き方が違うだけ」に見えますが、コンパイラの裏側や、実際の開発現場では決定的な違いがあります。その正体が「Bivariance(双方向性)」という型システムの挙動です。
—
2. コンパイラが見ている世界:メソッド構文の「特権」
TypeScriptのコンパイラ(型チェッカー)にとって、この2つの書き方は全く異なる扱いに変換されます。
一番大きな違いは、メソッド構文(パターンA)には「引数の型がBivariant(双変)になる」という特権(あるいは仕様の癖)があるという点です。
言葉だけだと難しいので、コードで見てみましょう。`strictFunctionTypes`(厳格な関数型のチェック)が有効な環境を想像してください。
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
// 1. プロパティ構文の場合(コントラバリアンス:反変性が厳格に守られる)
interface HandlerB {
process: (animal: Animal) => void;
}
let handleAnimal: HandlerB = {
process: (animal: Animal) => { console.log(animal.name); }
};
// ❌ コンパイルエラーになる!
// 「DogはAnimalのすべてのプロパティを持っているが、関数の引数としては安全ではない」と判定されるため。
// let handleDog: HandlerB = {
// process: (dog: Dog) => { dog.bark(); }
// };
なぜエラーになるかと言うと、関数型プログラミングの厳密なルール(反変性)では、「より広い型(`Animal`)を受け取れる関数を入れるべき場所に、より狭い型(`Dog`)専用の関数を代入するのは危険」だからです(もし猫が渡ってきたら、`bark`メソッドがなくてクラッシュしてしまいますよね)。
しかし! メソッド構文(パターンA)を使うと、このチェックが少し緩くなります(双変になる)。
interface HandlerA {
process(animal: Animal): void;
}
// ⭕️ メソッド構文だと、なぜかエラーにならない!
let handleDogSafe: HandlerA = {
process(dog: Dog) {
dog.bark();
}
};
「えっ、エラーにならない方が便利じゃん!」と思いましたか?
実はこれ、古いJavaScriptのライブラリ(配列のコールバックなどで親クラスを期待する場所に子クラスを渡すケースなど)との互換性のために残されている仕様です。しかし、現代の厳密な型安全性を求める現場では、意図しないバグ(実行時エラー)の温床になることがあります。
—
3. 陥りやすい文法エラーと罠:`this` の扱いに要注意!
もう一つの大きな違いは、スコープの中の `this` の扱いです。
メソッド構文の `this`
メソッド構文で定義された関数内では、`this` はそのオブジェクト自身を指すように暗黙的に型付けされます。
interface Counter {
count: number;
increment(): void;
}
const myCounter: Counter = {
count: 0,
increment() {
// ⭕️ この this は myCounter を指すことが保証される
this.count++;
}
};
プロパティ構文の罠
一方、プロパティ構文(アロー関数など)で `this` を使おうとすると、思わぬ罠にハマります。
interface BadCounter {
count: number;
increment: () => void;
}
const badCounter: BadCounter = {
count: 0,
increment: () => {
// ⚠️ アロー関数の this は「定義されたスコープ(通常はグローバルや外側)」を向く!
// 厳格モード(strict)では this が undefined になり、実行時エラーになるか、
// 型エラーとして指摘されます。
// this.count++;
}
};
もしプロパティ構文でオブジェクト自身のメソッドを表現したい場合は、明示的に `this` の型を第1引数に書いてあげる必要があります(Explicit This)。
interface GoodCounter {
count: number;
// 第1引数に this の型を明示する
increment: (this: GoodCounter) => void;
}
—
4. 現場で迷ったときの設計指針
「じゃあ、結局どっちを使えばいいの?」という疑問が湧きますよね。
シニアアーキテクトとしての実践的な指針をシェアします。
1. 「オブジェクトの振る舞い(振る舞いを持つデータ)」を定義するなら:メソッド構文(パターンA)
- 例:サービスクラス、DOM要素のラッパー、何らかの状態を書き換えるオブジェクトなど。
- オブジェクト指向的な「メソッド呼び出し(`obj.method()`)」を期待する場合はこちらが自然です。
2. 「コールバック関数」や「純粋な関数、イベントハンドラ」を保持するなら:プロパティ構文(パターンB)
- 例:Reactのカスタムフックの戻り値、APIクライアントの設定(`onClick: (e: MouseEvent) => void`)、ストアの状態管理など。
- 関数型プログラミングのスタイルを取り入れる場合や、アロー関数として渡されることを前提とする場合は、型が厳格にチェックされるプロパティ構文(`prop: (args) => void`)が安全です。
—
まとめ
今回は Interface 内のメソッド定義における2つのアプローチについて解説しました。
- メソッド構文 (`func(arg: T): U`):
- オブジェクト指向的。
- 引数の型チェックが少し緩くなる(双変性)。
- `this` がオブジェクト自身を向く。
- プロパティ構文 (`func: (arg: T) => U`):
- 関数型・モダンJS的。
- 引数の型チェックが厳格(反変性)。
- `this` の扱いに注意が必要(アロー関数になると外側を向く)。
一見すると些細なシンタックスの違いに見えますが、TypeScriptの型システムがコンパイル時にどう解釈するかを知ることで、より堅牢で予測可能なコードを書けるようになります。
ここをクリアしたあなたなら、もうTypeScriptの型システムのクセで迷うことはありません!
明日からのコーディングで、ぜひ意識して使い分けてみてくださいね。それでは、快適なTypeScriptライフを!