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#のノミナルタイピングとの大きな違いです。