はじめに:同じ名前でも役割が違う?
弊社のほとんどのプロダクトでは、バックエンドに PHP、フロントエンドに TypeScript を採用しています。
この2つの言語を行き来して開発を進める中で、特に混乱を招きやすいポイントがあります。
それが 「interface(インターフェース)」 です。
同じ単語、似たような構文を使っているのに、その役割や挙動は根本的に異なります。
この違いを曖昧にしたまま開発を進めると、TypeScriptで「なぜここでエラーが出ないんだ?」と戸惑ったり、逆にPHPの感覚で厳格に書こうとしてコードが必要以上に冗長になったりします。
この記事では、両者の interface の本質的な違いを明らかにし、それぞれの言語における適切な付き合い方を整理します。
1. 根本的な違い:「保証するもの」が違う
結論から言うと、両者の違いは「何を保証しようとしているか」の一点に集約されます。
| 項目 | オブジェクト指向の interface (PHPなど) | TypeScriptの interface |
|---|---|---|
| 保証の対象 | 振る舞い(メソッドの契約) | データ(プロパティの形) |
| 判断の思想 | 名前的型付け(Nominal Typing) | 構造的型付け(Structural Typing) |
| 判定主体 | クラス自身の「自己申告」 | 利用側の「外部認定」 |
| 存在時期 | 実行時にも情報が残る | コンパイル時に消滅する |
オブジェクト指向(OOP)における一般的な「クラスの実装を強制するもの」という役割に対し、TypeScriptではもう少し柔軟な、データ構造のための役割を持っています。
2. 型付けの思想:「自己申告」vs「外部認定」
なぜここまで挙動が違うのでしょうか?それは「型をどう判定するか」という思想が真逆だからです。
2.1. PHP/OOP:厳格な「自己申告」(名前的型付け)
OOP(PHPなど)の interface は、クラスが「私はこの契約を守っています」と 自己申告(implements) することで初めて型として成立します。
- 例えるなら:「犬は動物である」の関係
-
キーワード:
B is A(クラスBはインターフェースAの系譜である)
どんなに中身(メソッド)が同じでも、implements と書いていないクラスは別物として扱われます。名前(ラベル)が一致していることが全てです。
2.2. TypeScript:柔軟な「外部認定」(構造的型付け)
TypeScriptの interface は、利用側がデータの形(構造)を見て型を認定します。
- 例えるなら:「AIBO(ロボット犬)も動物として扱う」の関係
-
キーワード:
B as A(オブジェクトBはインターフェースAの形をしている)
クラスによる自己申告は必須ではありません。「bark()(吠える)メソッドを持っていて、name プロパティがあるなら、それはもう Dog とみなしてOK」という考え方です。
「なぜここでエラーが出ない?」の正体
この思想のため、TSでは以下のようなコードがエラーになりません。
interface User {
name: string;
}
// User型にはない age を持っているオブジェクト
const detailedUser = { name: "Tanaka", age: 25 };
// エラーにならない
// (User型に必要な name を持っているため、Userとして扱える)
const user: User = detailedUser;
PHP脳だと「ageなんて定義してないぞ!」と違和感を覚えますが、TypeScriptは「nameさえあればUserとしての要件は満たしている」と判断します。
これが構造的型付け(Structural Typing)です。
図解:関係性の違い
3. 実践コード:存在時期の違いとチェック
interface が実行時に存在するか否かの違いは、コードの書き方に大きく影響します。
3.1. PHP:interface は実行時にも情報が残る
PHPのインターフェースは実行時にもメタデータとして情報が残っています。そのため、動的に instanceof でチェックすることが可能です。
interface Animal {}
class Dog implements Animal {}
$obj = new Dog();
// 実行時に $obj が Animal の系譜かチェックできる
if ($obj instanceof Animal) {
echo "これは動物です";
}
3.2. TypeScript:interface はコンパイル時に消滅する
ここが最大の落とし穴です。TypeScriptのインターフェースはJavaScriptに変換された時点で完全に消滅します。
instanceof はクラス(コンストラクタ)の存在を確認するものなので、消えてしまった interface を判定に使うことはできません。
interface Animal {
name: string;
}
class Dog implements Animal {
name = "Pochi";
}
const obj = new Dog();
// ❌ コンパイルエラー!
// インターフェースはコンパイル時に消えてしまうためinstanceofでPHPのように判定できない
if (obj instanceof Animal) {
// TS2693: Animal only refers to a type, but is being used as a value here.
}
解決策:ユーザー定義型ガード
ではどうやって判定するのか?「ユーザー定義型ガード」を使います。
「コンパイラには分からないけど、ロジックとしてこの条件を満たせばAnimal型とみなすよ」という関数を自分で定義します。
// 「戻り値がtrueなら、引数argはAnimal型である(arg is Animal)」と宣言
function isAnimal(arg: any): arg is Animal {
return arg !== null && typeof arg === "object" && "name" in arg;
}
if (isAnimal(obj)) {
console.log(obj.name); // ここでは安全に Animal として扱える
}
4. なぜTSは「データの保証」を選んだのか?
なぜTypeScriptは、OOPのような厳格さを捨ててまで、構造的型付け(形による判定)を採用したのでしょうか?
それは、TypeScriptが フロントエンド開発(JavaScriptの世界) の課題を解決するために生まれたからです。
4.1. 課題:OOPの「振る舞い保証」ではデータ定義が非効率
フロントエンドでは、外部APIから返ってくるJSONデータなど、不定形なデータを扱う頻度が非常に高いです。
もしOOPのように「名前的型付け」しかできない場合、APIから受け取る「ただのデータ構造」のためだけに、いちいちクラスを定義し、データを詰め替える必要があります。これは大量の ボイラープレート(定型コード) を生み出します。
4.2. データ中心の設計
「APIから { name: string, age: number } というデータが来る」
この事実だけを保証したい場合、クラス階層は邪魔になります。
TypeScriptは、「データの形(スキーマ)」だけをサッと定義し、それが満たされていれば安全にコードが書けるように設計されました。これにより、JSONデータに対してクラスを作ることなく、型安全性を享受できるのです。
5. 【実践】お互いの interface を再現してみる
理解を深めるために、あえて「相手の言語のやり方」を再現しようとすると、その違いが浮き彫りになります。
5.1. PHPの「振る舞い」をTSで再現するのは...簡単☺️
TypeScriptでもOOP的な書き方は可能です。
// implementsを使えば、PHP同様に「振る舞いの契約」として機能する
interface Walkable {
walk(distance: number): void;
}
// クラスが契約を履行(ここでメソッドがないとコンパイルエラー)
class Human implements Walkable {
public walk(distance: number): void {
console.log(`${distance}m 歩きました`);
}
}
// 注釈:HumanはWalkable(歩くことができる)という「is-a」関係が成立している
5.2. TSの「データの形だけ保証」をPHPで再現するのは...困難😭
逆に、PHPで「クラス定義なしで、データの形だけを保証する」のは非常に困難です。
【失敗例】無理やりinterfaceを使って「データの形」を保証しようとした場合
PHPには構造的型付けがないため、interfaceでデータの形を保証しようとすると、以下のような 「型定義のためのクラス地獄」 に陥りがちです。
// 1. interfaceにはプロパティが定義できないので、Getterメソッドを定義するしかない
interface UserDataInterface {
public function getName(): string;
public function getAge(): int;
}
// 2. 毎回Getterを書くのは面倒なので、Abstract Classを作る(あるある)
abstract class AbstractUserData implements UserDataInterface {
public function __construct(
protected string $name,
protected int $age
) {}
public function getName(): string { return $this->name; }
public function getAge(): int { return $this->age; }
}
// 3. やっと実体クラスを作る
class UserResponse extends AbstractUserData {}
// 結論:たった2つのデータを受け取るために、ここまでコードを書く必要があります。
// TSなら interface User { name: string; age: number; } の1行で済む話。
これが設計思想の違いによる「書きやすさ」の差です。
まとめ:コミュニケーションのズレを解消する
-
PHP (OOP) の interface:
- 「振る舞い(メソッド)」 を定義する。
- クラスの設計や責務を強制するために使う。
-
TypeScript の interface:
- 「データ構造(プロパティ)」 を定義する。
- APIレスポンスやpropsなど、データの形を保証するために使う。
この違いを理解していないと、バックエンド(PHP)とフロントエンド(TS)の開発者間で会話がかみ合わなくなります。
「インターフェース作っておいて」と言われたとき、PHPエンジニアは「クラス設計」を考え、TSエンジニアは「データ定義」を考えます。
このギャップを意識し、「TSのinterfaceは、APIスキーマのようなデータの型紙だ」 と割り切って使うことで、TypeScriptの柔軟性を最大限に活かせるようになります。