2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHP vs TS】同じ interface なのに別物?「振る舞いの保証」と「データの保証」の本質的な違い

Posted at

はじめに:同じ名前でも役割が違う?

弊社のほとんどのプロダクトでは、バックエンドに 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 でチェックすることが可能です。

php
interface Animal {}
class Dog implements Animal {}

$obj = new Dog();

// 実行時に $obj が Animal の系譜かチェックできる
if ($obj instanceof Animal) {
    echo "これは動物です";
}

3.2. TypeScript:interface はコンパイル時に消滅する

ここが最大の落とし穴です。TypeScriptのインターフェースはJavaScriptに変換された時点で完全に消滅します。
instanceof はクラス(コンストラクタ)の存在を確認するものなので、消えてしまった interface を判定に使うことはできません。

typescript
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.
}

参考: instanceofとインターフェース | サバイバルTypeScript

解決策:ユーザー定義型ガード

ではどうやって判定するのか?「ユーザー定義型ガード」を使います。
「コンパイラには分からないけど、ロジックとしてこの条件を満たせばAnimal型とみなすよ」という関数を自分で定義します。

typescript
// 「戻り値が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的な書き方は可能です。

typescript
// 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でデータの形を保証しようとすると、以下のような 「型定義のためのクラス地獄」 に陥りがちです。

php
// 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の柔軟性を最大限に活かせるようになります。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?