TypeScriptインターフェースとC#インターフェースの主な違い:構造的部分型 - hideki5123/myts GitHub Wiki

課題の例で let box: Box = { height: 5, width: 6, scale: 10 }; のように書けるのはなぜでしょうか? C#であれば class MyBox : IBox { ... } のように明示的にインターフェースを実装する必要がありますが、TypeScriptでは必ずしもそうではありません。これは、TypeScriptが採用している構造的部分型 (Structural Typing) という仕組みに基づいています。

1. TypeScriptの型の互換性:構造で判断 (Structural Typing)

  • TypeScriptは、オブジェクトの**「構造」や「形状 (Shape)」**がインターフェースの定義と一致するかどうかで、型の互換性を判断します。
  • あるオブジェクトが、インターフェースで定義されているプロパティやメソッドを(正しい型で)持っているならば、たとえ implements のような明示的な宣言がなくても、そのインターフェースと互換性があるとみなされます。
  • 例 (TypeScript):
    interface Box {
        height: number;
        width: number;
        scale: number;
    }
    
    // オブジェクトリテラルの構造がBoxインターフェースと一致するのでOK
    let box1: Box = { height: 5, width: 6, scale: 10 };
    
    class SomeOtherBox {
        height = 7;
        width = 8;
        scale = 9;
        // color = "red"; // 余計なプロパティがあってもOK (代入先の型が要求するものを満たしていれば)
    }
    // SomeOtherBox のインスタンスも Box の構造を持っているので代入可能 (implements 不要)
    let box2: Box = new SomeOtherBox();
    
    • let box1: Box = { ... } では、{...} のオブジェクトリテラルの構造が Box インターフェースの定義と一致するかを直接チェックします。
    • let box2: Box = new SomeOtherBox() では、SomeOtherBox クラスのインスタンスが Box の要求する height, width, scale を持っているため、implements Box と書いていなくても代入可能です。

2. C#の型の互換性:名前と宣言で判断 (Nominal Typing)

  • C#は、「名前」と**「明示的な宣言」**で互換性を判断します(ノミナルタイピング)。
  • class Hoge : IHoge のように、クラスがインターフェースを実装すると宣言しない限り、互換性があるとはみなされません。たとえ必要なメンバーをすべて持っていても、宣言がなければ代入できません。
  • 例 (C#):
    // C# インターフェース
    public interface IBox
    {
        int Height { get; set; }
        int Width { get; set; }
        int Scale { get; set; }
    }
    
    // 明示的に IBox を実装するクラス
    public class MyBox : IBox
    {
        public int Height { get; set; }
        public int Width { get; set; }
        public int Scale { get; set; }
    }
    
    // IBox と同じ構造を持つが、明示的な実装宣言がないクラス
    public class AnotherBox
    {
        public int Height { get; set; }
        public int Width { get; set; }
        public int Scale { get; set; }
    }
    
    public class Test
    {
        public void Run()
        {
            // OK: MyBox は IBox を明示的に実装している
            IBox box1 = new MyBox { Height = 5, Width = 6, Scale = 10 };
    
            AnotherBox tempBox = new AnotherBox { Height = 7, Width = 8, Scale = 9 };
    
            // コンパイルエラー!: AnotherBox は IBox を実装すると宣言していないため、
            // たとえ構造が同じでも IBox 型の変数には代入できない。
            // IBox box2 = tempBox; // エラー CS0266
            // IBox box3 = new AnotherBox { Height = 7, Width = 8, Scale = 9 }; // エラー CS0266
        }
    }
    

3. TypeScriptにおける implements キーワードの役割

  • TypeScriptにも class 定義で使う implements キーワードは存在します。
    interface Printable { print(): void; }
    class Report implements Printable { // implementsキーワードを使用
        print() { console.log("Printing report..."); }
    }
    
  • クラスで implements を使う主な目的は以下の2つです。
    • 実装チェック: クラスがインターフェースの要件をすべて満たしているか、コンパイラにチェックさせるため。実装漏れを防ぎます。
    • 意図の明確化: クラスが特定の規約に従うことを、コードを読む人に明確に伝えるため。
  • 重要: たとえ implements を書かなくても、クラスが必要な構造(例: print() メソッド)を持っていれば、そのクラスのインスタンスはインターフェース型(例: Printable)の変数に代入可能です(構造的部分型のため)。implements は主にクラス定義時の補助的なチェック機能です。

結論

オブジェクトリテラルやクラスインスタンスをインターフェース型の変数に代入する際、TypeScriptはC#のような明示的な implements 宣言を必須としません。オブジェクトがインターフェースの定義する 構造(プロパティやメソッドの形状) を持っているかどうかで型の互換性を判断します。これが構造的部分型の基本的な考え方であり、C#のノミナルタイピングとの大きな違いです。