継承とインターフェース
この章では、TypeScriptにおけるクラスの継承とインターフェースについて学びます。
この章で学ぶこと
- 継承(extends)の使い方
- 抽象クラス(abstract)の定義
- インターフェース(interface)の基本
- implementsキーワードによるインターフェースの実装
この章のコード例は、TypeScript Playgroundで試すことができます。
継承(extends)
継承とは、既存のクラスの機能を引き継いで新しいクラスを作成する仕組みです。コードの再利用性を高め、クラス間の関係性を明確にできます。
基本的な継承
// 親クラス(基底クラス、スーパークラス)
class Animal {
constructor(
public name: string,
protected age: number
) {}
makeSound(): void {
console.log('Some generic sound');
}
sleep(): void {
console.log(`${this.name} is sleeping`);
}
}
// 子クラス(派生クラス、サブクラス)
class Dog extends Animal {
constructor(name: string, age: number, public breed: string) {
// 親クラスのコンストラクタを呼び出す(必須)
super(name, age);
}
// メソッドのオーバーライド(上書き)
makeSound(): void {
console.log('Woof! Woof!');
}
// 子クラス独自のメソッド
fetch(): void {
console.log(`${this.name} is fetching the ball`);
}
}
class Cat extends Animal {
constructor(name: string, age: number) {
super(name, age);
}
makeSound(): void {
console.log('Meow! Meow!');
}
scratch(): void {
console.log(`${this.name} is scratching`);
}
}
// 使用例
const dog = new Dog('Buddy', 3, 'Golden Retriever');
dog.makeSound(); // 'Woof! Woof!'
dog.sleep(); // 'Buddy is sleeping'(親クラスのメソッド)
dog.fetch(); // 'Buddy is fetching the ball'
const cat = new Cat('Whiskers', 2);
cat.makeSound(); // 'Meow! Meow!'
cat.sleep(); // 'Whiskers is sleeping'
cat.scratch(); // 'Whiskers is scratching'
コード解説:
extendsキーワードで継承を宣言super(...)で親クラスのコンストラクタを呼び出し(子クラスのコンストラクタ内で最初に必須)- 親クラスのメソッドをオーバーライド(上書き)可能
- 親クラスの
publicとprotectedメンバーにアクセス可能 privateメンバーには子クラスからもアクセスできない
superによる親クラスのメソッド呼び出し
オーバーライドしたメソッド内で、親クラスの実装を呼び出すこともできます。
class Vehicle {
constructor(
public brand: string,
protected speed: number = 0
) {}
accelerate(amount: number): void {
this.speed += amount;
console.log(`Speed increased to ${this.speed} km/h`);
}
brake(amount: number): void {
this.speed = Math.max(0, this.speed - amount);
console.log(`Speed decreased to ${this.speed} km/h`);
}
}
class Car extends Vehicle {
constructor(brand: string, private model: string) {
super(brand);
}
// 親クラスのメソッドをオーバーライドしつつ、親の処理も実行
accelerate(amount: number): void {
console.log(`${this.brand} ${this.model} is accelerating...`);
// 親クラスのaccelerateメソッドを呼び出す
super.accelerate(amount);
}
showInfo(): void {
console.log(`Car: ${this.brand} ${this.model}, Speed: ${this.speed} km/h`);
}
}
const car = new Car('Toyota', 'Camry');
car.accelerate(50);
// 'Toyota Camry is accelerating...'
// 'Speed increased to 50 km/h'
car.accelerate(30);
// 'Toyota Camry is accelerating...'
// 'Speed increased to 80 km/h'
car.showInfo(); // 'Car: Toyota Camry, Speed: 80 km/h'
抽象クラス(abstract)
抽象クラスは、インスタンス化できないクラスで、継承先で実装すべきメソッドを定義します。「設計の枠組み」として機能します。
抽象クラスの基本
// 抽象クラス: abstractキーワードを付ける
abstract class Shape {
constructor(public name: string) {}
// 抽象メソッド: 実装を持たず、継承先で必ず実装する必要がある
abstract calculateArea(): number;
abstract calculatePerimeter(): number;
// 通常のメソッド: 実装を持つ
describe(): void {
console.log(`This is a ${this.name}`);
console.log(`Area: ${this.calculateArea()}`);
console.log(`Perimeter: ${this.calculatePerimeter()}`);
}
}
// 抽象クラスはインスタンス化できない
// const shape = new Shape('shape'); // Error: Cannot create an instance of an abstract class
// 抽象クラスを継承したクラス
class Circle extends Shape {
constructor(private radius: number) {
super('Circle');
}
// 抽象メソッドを実装(必須)
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super('Rectangle');
}
calculateArea(): number {
return this.width * this.height;
}
calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
}
// 使用例
const circle = new Circle(10);
circle.describe();
// 'This is a Circle'
// 'Area: 314.159...'
// 'Perimeter: 62.831...'
const rectangle = new Rectangle(5, 10);
rectangle.describe();
// 'This is a Rectangle'
// 'Area: 50'
// 'Perimeter: 30'
抽象クラスの特徴:
abstractキーワードで定義- インスタンス化できない(継承して使う)
- 抽象メソッドは実装を持たない(宣言だけ)
- 継承先で抽象メソッドを必ず実装する必要がある
- 通常のメソッド(実装あり)も定義できる
実践的な例: 支払い処理
// 抽象的なPaymentクラス
abstract class Payment {
constructor(
protected amount: number,
protected currency: string = 'JPY'
) {}
// 抽象メソッド: 各支払い方法で実装が異なる
abstract process(): Promise<boolean>;
abstract getReceipt(): string;
// 共通のメソッド
getAmount(): string {
return `${this.amount} ${this.currency}`;
}
protected logTransaction(success: boolean): void {
const status = success ? 'SUCCESS' : 'FAILED';
console.log(`[${new Date().toISOString()}] Payment ${status}: ${this.getAmount()}`);
}
}
// クレジットカード支払い
class CreditCardPayment extends Payment {
constructor(
amount: number,
private cardNumber: string,
private expiryDate: string
) {
super(amount);
}
async process(): Promise<boolean> {
// 実際にはカード会社のAPIを呼び出す
console.log(`Processing credit card payment: ${this.maskCardNumber()}`);
const success = true; // シミュレーション
this.logTransaction(success);
return success;
}
getReceipt(): string {
return `Credit Card Payment: ${this.getAmount()} (Card: ${this.maskCardNumber()})`;
}
private maskCardNumber(): string {
return '**** **** **** ' + this.cardNumber.slice(-4);
}
}
// 銀行振込
class BankTransferPayment extends Payment {
constructor(
amount: number,
private bankCode: string,
private accountNumber: string
) {
super(amount);
}
async process(): Promise<boolean> {
console.log(`Processing bank transfer to: ${this.bankCode}-${this.accountNumber}`);
const success = true;
this.logTransaction(success);
return success;
}
getReceipt(): string {
return `Bank Transfer: ${this.getAmount()} (Account: ${this.accountNumber})`;
}
}
// 使用例
async function processPayments() {
const payments: Payment[] = [
new CreditCardPayment(5000, '1234567890123456', '12/25'),
new BankTransferPayment(10000, '0001', '1234567')
];
for (const payment of payments) {
await payment.process();
console.log(payment.getReceipt());
console.log('---');
}
}
processPayments();
インターフェース(interface)
インターフェースは、オブジェクトの「形」や「契約」を定義する仕組みです。クラスや値がどのような構造を持つべきかを宣言します。
インターフェースの基本
// インターフェースの定義
interface User {
id: number;
name: string;
email: string;
age?: number; // オプショナルプロパティ
}
// インターフェースを満たすオブジェクト
const user1: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
age: 25
};
const user2: User = {
id: 2,
name: 'Bob',
email: 'bob@example.com'
// ageは省略可能
};
// 関数の引数の型としても使える
function printUser(user: User): void {
console.log(`${user.id}: ${user.name} (${user.email})`);
}
printUser(user1); // '1: Alice (alice@example.com)'
printUser(user2); // '2: Bob (bob@example.com)'
メソッドを持つインターフェース
interface Printable {
print(): void;
}
interface Saveable {
save(): boolean;
}
// インターフェースを満たすオブジェクト
const document: Printable & Saveable = {
print() {
console.log('Printing document...');
},
save() {
console.log('Saving document...');
return true;
}
};
document.print(); // 'Printing document...'
document.save(); // 'Saving document...'
インターフェースの継承
インターフェースは他のインターフェースを継承できます。
interface Named {
name: string;
}
interface Aged {
age: number;
}
// 複数のインターフェースを継承
interface Person extends Named, Aged {
email: string;
}
const person: Person = {
name: 'Alice',
age: 25,
email: 'alice@example.com'
};
implementsキーワード
クラスはインターフェースを**実装(implements)**できます。これにより、クラスがインターフェースで定義された契約を満たすことを保証します。
基本的な使い方
interface Printable {
print(): void;
}
interface Saveable {
save(): void;
}
// 複数のインターフェースを実装
class Document implements Printable, Saveable {
constructor(private content: string) {}
// Printableインターフェースのメソッドを実装
print(): void {
console.log(`Printing: ${this.content}`);
}
// Saveableインターフェースのメソッドを実装
save(): void {
console.log(`Saving: ${this.content}`);
}
}
const doc = new Document('Hello, World!');
doc.print(); // 'Printing: Hello, World!'
doc.save(); // 'Saving: Hello, World!'
実践的な例: リポジトリパターン
// データ操作のインターフェース
interface Repository<T> {
findAll(): T[];
findById(id: number): T | undefined;
create(item: T): T;
update(id: number, item: Partial<T>): T | undefined;
delete(id: number): boolean;
}
// ユーザーエンティティ
interface User {
id: number;
name: string;
email: string;
}
// メモリ上で動作するUserリポジトリ
class InMemoryUserRepository implements Repository<User> {
private users: User[] = [];
private nextId: number = 1;
findAll(): User[] {
return [...this.users];
}
findById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}
create(item: Omit<User, 'id'>): User {
const newUser: User = {
id: this.nextId++,
...item
};
this.users.push(newUser);
return newUser;
}
update(id: number, item: Partial<User>): User | undefined {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) return undefined;
this.users[index] = { ...this.users[index], ...item };
return this.users[index];
}
delete(id: number): boolean {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
return true;
}
}
// 使用例
const userRepo = new InMemoryUserRepository();
const alice = userRepo.create({ name: 'Alice', email: 'alice@example.com' });
console.log(alice); // { id: 1, name: 'Alice', email: 'alice@example.com' }
const bob = userRepo.create({ name: 'Bob', email: 'bob@example.com' });
console.log(userRepo.findAll()); // [alice, bob]
userRepo.update(1, { name: 'Alicia' });
console.log(userRepo.findById(1)); // { id: 1, name: 'Alicia', email: 'alice@example.com' }
userRepo.delete(2);
console.log(userRepo.findAll()); // [alice]
抽象クラス vs インターフェース
| 特徴 | 抽象クラス | インターフェース |
|---|---|---|
| インスタンス化 | できない | できない |
| 実装 | 持てる | 持てない(型定義のみ) |
| 継承 | 単一継承のみ | 複数実装可能 |
| コンストラクタ | 持てる | 持てない |
| アクセス修飾子 | 使える | 使えない |
| 用途 | 共通の実装を提供 | 契約・型の定義 |
// 抽象クラス: 共通の実装を持つ
abstract class Animal {
constructor(protected name: string) {}
// 共通の実装
sleep(): void {
console.log(`${this.name} is sleeping`);
}
// 抽象メソッド
abstract makeSound(): void;
}
// インターフェース: 契約のみ
interface Flyable {
fly(): void;
}
// 両方を使用
class Bird extends Animal implements Flyable {
constructor(name: string) {
super(name);
}
makeSound(): void {
console.log('Tweet!');
}
fly(): void {
console.log(`${this.name} is flying`);
}
}
const bird = new Bird('Sparrow');
bird.sleep(); // 'Sparrow is sleeping'(抽象クラスから継承)
bird.makeSound(); // 'Tweet!'(抽象メソッドを実装)
bird.fly(); // 'Sparrow is flying'(インターフェースを実装)
試してみよう: 図形クラスを設計しよう ★★★
以下の要件を満たす図形クラス群を設計してください。
要件:
-
インターフェース
Drawable:draw(): voidメソッド
-
抽象クラス
Shape:name: stringプロパティ(readonly)abstract calculateArea(): number抽象メソッドabstract calculatePerimeter(): number抽象メソッドdescribe(): voidメソッド(名前、面積、周囲長を表示)Drawableインターフェースを実装
-
Circleクラス(Shapeを継承):radius: numberプロパティ- 面積 = π × r²
- 周囲長 = 2 × π × r
-
Rectangleクラス(Shapeを継承):width: number,height: numberプロパティ- 面積 = width × height
- 周囲長 = 2 × (width + height)
-
Triangleクラス(Shapeを継承):a: number,b: number,c: number(3辺の長さ)- 面積 = ヘロンの公式を使用
- 周囲長 = a + b + c
ヒント
- ヘロンの公式: s = (a + b + c) / 2, 面積 = √(s × (s-a) × (s-b) × (s-c))
- 各図形の
draw()メソッドでは、図形をASCIIアートで表示するのもよい - コンストラクタの短縮記法を活用する
回答と解説
// 描画可能なオブジェクトのインターフェース
interface Drawable {
draw(): void;
}
// 図形の抽象クラス
abstract class Shape implements Drawable {
constructor(public readonly name: string) {}
// 抽象メソッド: 継承先で必ず実装
abstract calculateArea(): number;
abstract calculatePerimeter(): number;
// Drawableインターフェースを実装(抽象メソッドとして継承先に委譲も可)
abstract draw(): void;
// 共通のメソッド
describe(): void {
console.log(`=== ${this.name} ===`);
console.log(`Area: ${this.calculateArea().toFixed(2)}`);
console.log(`Perimeter: ${this.calculatePerimeter().toFixed(2)}`);
}
}
// 円クラス
class Circle extends Shape {
constructor(private radius: number) {
super('Circle');
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
draw(): void {
// 簡易的なASCIIアート
console.log(' *** ');
console.log(' * * ');
console.log(' * * ');
console.log(' * * ');
console.log(' * * ');
console.log(' *** ');
}
}
// 長方形クラス
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super('Rectangle');
}
calculateArea(): number {
return this.width * this.height;
}
calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
draw(): void {
const horizontalLine = '+' + '-'.repeat(this.width) + '+';
console.log(horizontalLine);
for (let i = 0; i < Math.min(this.height, 5); i++) {
console.log('|' + ' '.repeat(this.width) + '|');
}
console.log(horizontalLine);
}
}
// 三角形クラス
class Triangle extends Shape {
constructor(
private a: number,
private b: number,
private c: number
) {
super('Triangle');
// 三角形の成立条件をチェック
if (!this.isValidTriangle()) {
throw new Error('Invalid triangle: sides do not form a valid triangle');
}
}
private isValidTriangle(): boolean {
return (
this.a + this.b > this.c &&
this.b + this.c > this.a &&
this.c + this.a > this.b
);
}
calculateArea(): number {
// ヘロンの公式
const s = (this.a + this.b + this.c) / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}
calculatePerimeter(): number {
return this.a + this.b + this.c;
}
draw(): void {
console.log(' * ');
console.log(' * * ');
console.log(' * * ');
console.log(' * * ');
console.log('*********');
}
}
// ポリモーフィズムを活用した関数
function displayShapes(shapes: Shape[]): void {
for (const shape of shapes) {
shape.draw();
shape.describe();
console.log('');
}
}
// テスト
const shapes: Shape[] = [
new Circle(5),
new Rectangle(8, 4),
new Triangle(3, 4, 5) // 直角三角形
];
displayShapes(shapes);
// 個別のテスト
console.log('=== Individual Tests ===');
const circle = new Circle(10);
console.log(`Circle area: ${circle.calculateArea().toFixed(2)}`); // 314.16
console.log(`Circle perimeter: ${circle.calculatePerimeter().toFixed(2)}`); // 62.83
const rectangle = new Rectangle(5, 10);
console.log(`Rectangle area: ${rectangle.calculateArea()}`); // 50
console.log(`Rectangle perimeter: ${rectangle.calculatePerimeter()}`); // 30
const triangle = new Triangle(3, 4, 5);
console.log(`Triangle area: ${triangle.calculateArea()}`); // 6
console.log(`Triangle perimeter: ${triangle.calculatePerimeter()}`); // 12
// エラーケース: 無効な三角形
try {
const invalidTriangle = new Triangle(1, 2, 10); // 成立しない
} catch (error) {
console.log((error as Error).message);
// 'Invalid triangle: sides do not form a valid triangle'
}
解説:
Drawableインターフェース:draw()メソッドの契約を定義Shape抽象クラス: 共通のdescribe()メソッドと抽象メソッドを定義- 各図形クラス: 抽象メソッドを具体的に実装
Triangleクラス: コンストラクタで三角形の成立条件をバリデーションdisplayShapes関数: ポリモーフィズムにより、異なる図形を同じ方法で処理
ポリモーフィズムのメリット:
Shape[]型の配列で異なる図形を統一的に扱える- 新しい図形クラスを追加しても、既存のコードを変更する必要がない
- インターフェースに依存することで、実装の詳細を隠蔽できる
まとめ
この章で学んだこと:
- 継承(extends): 既存のクラスの機能を引き継いで新しいクラスを作成
- super: 親クラスのコンストラクタやメソッドを呼び出す
- 抽象クラス(abstract): インスタンス化できない設計の枠組み、抽象メソッドは継承先で実装必須
- インターフェース(interface): オブジェクトの形や契約を定義
- implements: クラスがインターフェースを実装することを宣言
これらの概念を組み合わせることで、柔軟で拡張性のあるコードを書くことができます。次の章では、型の絞り込み(Type Narrowing)について学びます。
初学者がつまずきやすいポイント
よくある間違い
❌ 子クラスでsuperを呼び忘れる
class Animal {
constructor(public name: string) {}
}
// ❌ 間違い: superを呼んでいない
class Dog extends Animal {
constructor(name: string, public breed: string) {
// Error: Constructors for derived classes must contain a 'super' call
this.breed = breed;
}
}
// ✅ 正しい: 最初にsuperを呼ぶ
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // 親クラスのコンストラクタを最初に呼ぶ
}
}
原因: 子クラスのコンストラクタでは、thisを使う前にsuper()を呼ぶ必要があります。
解決策: 子クラスのコンストラクタの最初の行でsuper()を呼んでください。
❌ privateプロパティに子クラスからアクセス
class Parent {
private secret: string = 'hidden';
protected info: string = 'visible to children';
}
class Child extends Parent {
showInfo() {
// console.log(this.secret); // Error: privateは子クラスからアクセス不可
console.log(this.info); // OK: protectedは子クラスからアクセス可能
}
}
原因: privateは定義されたクラス内でのみアクセス可能で、子クラスからもアクセスできません。
解決策: 子クラスからアクセスさせたい場合はprotectedを使用してください。
❌ 抽象クラスをインスタンス化しようとする
abstract class Shape {
abstract calculateArea(): number;
}
// ❌ 間違い: 抽象クラスはインスタンス化できない
const shape = new Shape();
// Error: Cannot create an instance of an abstract class
// ✅ 正しい: 具象クラスを継承して実装
class Circle extends Shape {
constructor(private radius: number) {
super();
}
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5); // OK
原因: 抽象クラスは「設計図の設計図」であり、直接インスタンス化することを想定していません。
解決策: 抽象クラスを継承した具象クラスを作成し、そのクラスをインスタンス化してください。
❌ interfaceとtypeの違いを理解していない
// interface: 拡張可能(同名で宣言マージされる)
interface User {
name: string;
}
interface User {
age: number; // 同名interfaceは自動でマージされる
}
const user: User = { name: 'Alice', age: 25 }; // OK
// type: 拡張不可(同名で再定義するとエラー)
type Product = {
name: string;
};
// type Product = { // Error: 重複した識別子
// price: number;
// };
// typeの拡張は Intersection で行う
type ExtendedProduct = Product & { price: number };
原因: interfaceは同名宣言がマージされますが、typeは再定義できません。
解決策: オブジェクトの形状定義にはinterfaceを優先し、Union型やユーティリティ型にはtypeを使用してください。
❌ implementsとextendsを混同する
interface Printable {
print(): void;
}
class Document {
content: string = '';
}
// ❌ 間違い: interfaceをextendsしようとする(クラスには使えない)
// class Report extends Printable {} // Error
// ✅ 正しい: interfaceはimplementsで実装
class Report implements Printable {
print(): void {
console.log('Printing report...');
}
}
// ✅ 正しい: クラスはextendsで継承
class PDFDocument extends Document implements Printable {
print(): void {
console.log('Printing PDF...');
}
}
原因: extendsはクラスの継承、implementsはインターフェースの実装に使います。
解決策: クラス継承にはextends、インターフェース実装にはimplementsを使用してください。
Decorators(TC39 標準仕様 / TS 5.0+)
Decorator は、クラス・メソッド・プロパティに宣言的にメタデータや機能を付加するための構文です。TypeScript 5.0 以降、TC39 Stage 3 の標準仕様が stable として利用可能になりました。
// メソッドの呼び出しをログに残す Decorator の例
function log<T extends (this: unknown, ...args: unknown[]) => unknown>(
target: T,
context: ClassMethodDecoratorContext,
): T {
return function (this: unknown, ...args: unknown[]) {
console.log(`Calling ${String(context.name)} with`, args)
const result = target.apply(this, args)
console.log(`→ result:`, result)
return result
} as T
}
class Calculator {
@log
add(a: number, b: number) {
return a + b
}
}
new Calculator().add(2, 3)
// Calling add with [ 2, 3 ]
// → result: 5
旧 Decorator との違い
TypeScript には以前から experimental decorators(--experimentalDecorators)と呼ばれる古い実装がありましたが、これは TC39 標準と互換性がありません。
| 項目 | 標準 Decorators (TS 5.0+) | experimental decorators (legacy) |
|---|---|---|
| 仕様 | TC39 Stage 3 | 2015年頃の古い提案 |
| 有効化 | デフォルトで利用可 | --experimentalDecorators が必要 |
| 型引数 | ClassMethodDecoratorContext など標準型 | 独自の (target, key, descriptor) |
| メタデータ | Symbol.metadata が使える (TS 5.2+、未対応ランタイムでは polyfill が必要) | reflect-metadata ライブラリに依存 |
| 将来性 | JavaScript 標準に採用される予定 | 廃止方向 |
既存コードで --experimentalDecorators を使っている場合
Angular や NestJS、TypeORM など一部のフレームワークは今も legacy decorators を必要とします。フレームワークの移行計画に従って、対応バージョンが出るまで --experimentalDecorators を有効にしたままでも構いません。ただし新規プロジェクトでは TC39 標準 decorators を選んでください。
次に読む
型の絞り込み (Type Narrowing) へ進みます。Union型、型ガード(typeof / in / instanceof)、タグ付きUnionによる安全な型の絞り込みを学びます。
- 型の絞り込み (Type Narrowing) — 型ガードで Union 型を安全に扱う