TypeScript のクラスとインターフェース
クラスとインターフェースは、TypeScriptにおけるオブジェクト指向プログラミングの2つの柱です。クラスは実装を提供し、インターフェースは契約を定義します。クラスがインターフェースを実装する場合、TypeScriptはそのクラスがインターフェースで指定されたすべての要件を満たしていることを保証します。
1. クラスがインターフェースを実装する(implements)
(1) 基本的な構文
TYPESCRIPT
interface Printable {
toString(): string;
}
class User implements Printable {
constructor(public name: string, public age: number) {}
toString(): string {
return `${this.name},${this.age} years old`;
}
}
let user = new User("Charlie", 20);
console.log(user.toString()); // "Charlie,20 years old"
(2) インターフェースの実装の意義
インターフェースとは「契約」のことです。クラスがインターフェースを実装する場合、そのクラスは「このインターフェースで要求されるすべてのプロパティとメソッドを提供する」と約束することになります:
TYPESCRIPT
interface Animal {
name: string;
speak(): string;
move(distance: number): void;
}
class Dog implements Animal {
constructor(public name: string) {}
speak(): string {
return "Woof!";
}
move(distance: number): void {
console.log(`${this.name} Moved ${distance}m`);
}
}
// ❌ Members that do not meet the interface requirements will result in an error.
// class Cat implements Animal {
// constructor(public name: string) {}
// // Missing speak and move → Compilation Error
// }
(3) マルチインターフェースの実装
1つのクラスは、カンマで区切って複数のインターフェースを実装することができます:
TYPESCRIPT
interface Serializable {
serialize(): string;
}
interface Comparable {
compareTo(other: this): number;
}
class Score implements Serializable, Comparable {
constructor(public value: number) {}
serialize(): string {
return JSON.stringify({ value: this.value });
}
compareTo(other: Score): number {
return this.value - other.value;
}
}
let s1 = new Score(90);
let s2 = new Score(85);
console.log(s1.serialize()); // '{"value":90}'
console.log(s1.compareTo(s2)); // 5(Positive Number Display s1 > s2)
▶ 例:インターフェースを用いた統一APIの定義
TYPESCRIPT
interface Repository<T> {
findById(id: number): T | null;
findAll(): T[];
save(entity: T): void;
delete(id: number): boolean;
}
interface User {
id: number;
name: string;
email: string;
}
class UserRepository implements Repository<User> {
private users: User[] = [];
findById(id: number): User | null {
return this.users.find(u => u.id === id) ?? null;
}
findAll(): User[] {
return [...this.users];
}
save(entity: User): void {
let index = this.users.findIndex(u => u.id === entity.id);
if (index >= 0) {
this.users[index] = entity; // Update
} else {
this.users.push(entity); // New
}
}
delete(id: number): boolean {
let len = this.users.length;
this.users = this.users.filter(u => u.id !== id);
return this.users.length < len;
}
}
let repo = new UserRepository();
repo.save({ id: 1, name: "Charlie", email: "xiao@example.com" });
repo.save({ id: 2, name: "Diana", email: "hong@example.com" });
console.log(repo.findById(1)?.name); // "Charlie"
console.log(repo.findAll().length); // 2
出力:
TEXT
Charlie
2
2. インターフェースを継承するクラス
TypeScriptのユニークな特徴として、インターフェースはクラスからメンバー定義を継承することができます(型宣言のみを継承し、実装は継承しません):
TYPESCRIPT
class Control {
private state: string = "active";
protected getState(): string {
return this.state;
}
}
// Interface-Inherited Classes——Inherit only the type signature
interface Selectable extends Control {
select(): void;
}
// When a class implements an interface, it must provide all members.
class Dropdown implements Selectable {
private state: string = "active"; // You must implement it yourself. private Members
protected getState(): string {
return this.state;
}
select(): void {
console.log("Selected:" + this.getState());
}
}
💡 目的: インターフェースを継承するクラスは、主に複雑なクラス階層において、「特定のクラスの構造を持ちつつ、追加の機能も備えている」型を記述するために使用されます。日常的な開発ではあまり使用されません。
3. 抽象クラス (abstract)
抽象クラスとは「未完成のクラス」であり、部分的な実装しか提供せず、サブクラスが残りの部分を完成させなければなりません:
(1) 基本的な構文
TYPESCRIPT
abstract class Shape {
// Abstract Methods——Not implemented,Subclasses must implement
abstract getArea(): number;
abstract getPerimeter(): number;
// Specific Methods——Implemented,Direct Inheritance by Subclasses
describe(): string {
return `Area:${this.getArea().toFixed(2)},Perimeter:${this.getPerimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(public radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
// ❌ Abstract classes cannot be instantiated directly.
// let shape = new Shape();
let circle = new Circle(5);
let rect = new Rectangle(4, 6);
console.log(circle.describe()); // "Area:78.54,Perimeter:31.42"
console.log(rect.describe()); // "Area:24.00,Perimeter:20.00"
(2) 抽象クラスとインターフェース
| 機能 | 抽象クラス | インターフェース |
|---|---|---|
| 実装方法 | 特定の実装が存在する場合がある | いかなる実装も存在してはならない |
| コンストラクタ | 持つことができる | 持つことができない |
| 多重継承 | 単一継承のみ | 多重継承が可能(extends) |
| アクセス修飾子 | public/private/protected をサポート | プロパティはデフォルトで public です |
| インスタンス化 | 直接インスタンス化できない | インスタンス化できない(純粋型) |
| 実行時に存在するか | はい(JSクラスにコンパイルされる) | いいえ(純粋な型宣言) |
(3) 抽象クラスを使用するタイミング
- 複数のサブクラスが同じ部分実装を共有する(コードの再利用)
- プロテクトされたメンバは、サブクラスからのアクセスを許可する必要があります
- コンストラクタのロジックが必要
(4) インターフェースを使用するタイミング
- 「何ができるか」(機能の説明)を定義するだけでよい
- クラスは、複数の機能を実装する必要がある場合がある(複数のインターフェースの実装)。
- 実装コードを公開する必要はありません
- MERGE文が必要です
▶ 例:抽象クラスを使ったテンプレートメソッドパターンの実装
TYPESCRIPT
abstract class DataParser {
// Template Method——Define the Algorithm Framework
parse(input: string): string {
let raw = this.read(input);
let validated = this.validate(raw);
let transformed = this.transform(validated);
return this.format(transformed);
}
// Specific Methods——Shared by all subclasses
private read(input: string): string {
return input.trim();
}
// Abstract Methods——Each subclass implements it on its own
protected abstract validate(data: string): string;
protected abstract transform(data: string): string;
// Hook Method——Subclasses may choose to override this method
protected format(data: string): string {
return data;
}
}
class UpperParser extends DataParser {
protected validate(data: string): string {
if (data.length === 0) throw new Error("Input is empty");
return data;
}
protected transform(data: string): string {
return data.toUpperCase();
}
}
class ReverseParser extends DataParser {
protected validate(data: string): string {
return data;
}
protected transform(data: string): string {
return data.split("").reverse().join("");
}
protected format(data: string): string {
return `[Reversal] ${data}`;
}
}
let upper = new UpperParser();
let reverse = new ReverseParser();
console.log(upper.parse(" hello world ")); // "HELLO WORLD"
console.log(reverse.parse(" hello world ")); // "[Reversal] dlrow olleh"
出力:
TEXT
HELLO WORLD
[Reversal] dlrow olleh
4. クラスを型として使用する方法
クラス定義はそれ自体が型であり、変数の型としてクラス名を使用することができます:
(1) クラスの概念
TYPESCRIPT
class Point {
constructor(public x: number, public y: number) {}
distanceTo(other: Point): number {
return Math.sqrt((this.x - other.x) 2 + (this.y - other.y) 2);
}
}
// Class as a Type——Accept Point Instances or structurally compatible objects
let p1: Point = new Point(1, 2);
let p2: Point = { x: 3, y: 4 }; // ✅ Structural Compatibility(But does not include methods!)
console.log(p1.distanceTo(p2)); // ✅ p1 has distanceTo method
// p2.distanceTo(p1); // ❌ p2 Only x and y,There is no way
⚠️ 注意: クラスの「型」セクションには、インスタンスのプロパティとメソッドのシグネチャのみが含まれます。構造体互換のオブジェクトを代入する場合、メソッドは含まれず、プロパティのみが照合されます。メソッドにアクセスするには、
new Point() を使用してインスタンスを作成する必要があります。
(2) クラスタイプと typeof 演算子
TYPESCRIPT
class Factory {
static create(): Factory {
return new Factory();
}
constructor(public name: string) {}
}
// Factory Type——Instance Type
let instance: Factory = new Factory("Products");
// typeof Factory Type——The class itself(Constructor Types)
let FactoryClass: typeof Factory = Factory;
let another = FactoryClass.create(); // ✅ Calling a Static Method
❓ よくある質問
Q
implements と extends の違いは何ですか?A
implements は「インターフェースを実装する」という意味です。つまり、クラスはインターフェースで要求されるすべてのメンバを提供することを約束しますが、実装コードは一切継承しません。extendsは「クラスを継承する」ことを意味します。つまり、サブクラスはスーパークラスからすべての実装コードを継承し、それをオーバーライドすることができます。クラスは複数のインターフェースをimplementsできますが、スーパークラスは1つしかextendsできません。Q 抽象クラスとインターフェース、どちらを使うべきですか?
A 簡単なルールとして、複数のクラスで実装コードを共有する必要がある場合は抽象クラスを使い、「何ができるか」を規定する契約を定義するだけでよい場合はインターフェースを使います。この2つを組み合わせることも可能です。つまり、インターフェースで機能定義を行い、抽象クラスで部分的な実装を提供するという形です。
Q インターフェースを実装するクラスは、追加のメンバを持つことができますか?
A はい。インターフェースを実装するクラスは、「インターフェースで指定されたメンバーを少なくとも持つ」ことだけが求められており、追加のメンバーを持つことは全く問題ありません。例えば、
interface Printable { print(): void }とclass Document implements Printable { print() {...}; save() {...} }—Documentクラスには追加のsaveメソッドがありますが、これは全く問題ありません。Q インターフェースは抽象クラスから継承できますか?
A はい。
interface ISelect extends AbstractControl {} これは有効です。インターフェースは、抽象クラスのすべてのメンバのシグネチャ(プライベートメンバのシグネチャも含む)を継承します。ただし、このインターフェースを実装するクラスは、プライベートメンバを含め、すべてのメンバに対して独自の実装を提供する必要があります。📖 まとめ
implementsでは、クラスがインターフェースのすべてのメンバーを実装している必要があります。TypeScript はコンパイル時にこの要件が満たされているかを確認します。- クラスは、コンマで区切って複数のインターフェースを実装することができます。
- インターフェースはクラスを拡張することができます。ただし、継承されるのは型シグネチャのみであり、実装は継承されません。
- 抽象クラスは部分的な実装を提供します。サブクラスは、残りの抽象メソッドを実装する必要があります。
- 抽象クラスは実装コードの共有に適しており、インターフェースは機能の契約を定義するのに適しています。この2つは組み合わせて使用することができます。
- クラス定義はそれ自体が型であり、変数の型注釈として使用できます。
📝 練習問題
- 基本問題(難易度 ⭐):
Loggableインターフェース(logメソッドを含む)を定義し、UserクラスとProductクラスにそれぞれこのインターフェースを実装させます。インスタンスを作成し、logメソッドを呼び出してください。 - 上級問題(難易度 ⭐⭐): 抽象クラス
Vehicle(brandプロパティ、抽象メソッドstart、および具象メソッドdescribeを持つ)を定義し、それぞれstartメソッドを実装するサブクラスCarおよびBikeを作成してください。型Vehicleのパラメータを受け取り、describeメソッドを呼び出す関数を記述してください。 - 課題(難易度:⭐⭐⭐):
SortableCollection<T>インターフェース(lengthプロパティ、compareメソッド、およびswapメソッドを持つ)を設計し、NumberCollectionクラスにそれを実装させます。次に、Sorter抽象クラス内で(インターフェースのメソッドを使用して)バブルソートアルゴリズムを実装し、サブクラスがcompareおよびswapの具体的な実装のみを提供すればよいようにします。



