TypeScriptオライリー本第3弾、クラスとインターフェースの章。
クラス、抽象クラス、インターフェース、型エイリアスの違い等についてまとめる。
クラスのアクセス修飾子等
public | どこからでもアクセス可能。デフォルトはこれ |
---|---|
protected | このクラスとサブクラスのインスタンスからアクセス可能 |
private | このクラスのインスタンスのみアクセス可能 |
readonly | インスタンスプロパティを読み取りのみ可能・書き込み不可にする |
static | インスタンスを生成しなくても、そのクラスのプロパティ・メソッドを使用できるようになる |
abstract | 抽象クラス・抽象メソッドに指定する。そのサブクラスに対して抽象メソッドの実装を強制する |
抽象クラスの拡張の例。
abstract class MicrowaveOven { private power: number abstract heat(time: number, food: string): void } class MyMicrowaveOven extends MicrowaveOven { heat(time, food) { console.log(`${food}を${time}分あたためる`) } }
型エイリアスとインターフェースの違い
型エイリアス | インターフェース | |
---|---|---|
右辺に指定できる型 | 任意の型・型演算子 | 形状のみ |
拡張元から拡張先への割り当て | 自動的に両者の型を結合し、シグネチャをオーバーロードする | できない |
宣言のマージ | できない | 自動的に行われる |
1つ目の違いの具体例。インターフェースは{}で囲われた形のみ可能。型エイリアスの方がより柔軟に書ける。
type Girl = string type Female = Girl | number interface Soda { tansan: string } interface Cola extends Soda { spice: string }
2つ目の違いの具体例。本書指示どおり書いてみる。まずインターフェース。
interface A { good(x: number): string bad(x: number): string } interface B extends A { good(x: string | number): string bad(x: string): string } // インターフェイス 'B' はインターフェイス 'A' を正しく拡張していません。 // プロパティ 'bad' の型に互換性がありません。 // 型 '(x: string) => string' を型 '(x: number) => string' に割り当てることはできません。 // パラメーター 'x' および 'x' は型に互換性がありません。 // 型 'number' を型 'string' に割り当てることはできません。
次に型エイリアス。こちらはエラーが出ず通ってしまう。
オブジェクト型の継承を行うときは、エラーがチェックできるインターフェースを使用する方が安全。
type A = { good(x: number): string bad(x: number): string } type B = A & { good(x: string | number): string bad(x: string): string }
3つ目の違いの具体例。インターフェースの場合、同じ名前を複数宣言すると、自動的にプロパティがマージされる。
一方、型エイリアスは同じ名前を重複して宣言できない。
interface House { garden: boolean } interface House { parking: boolean } let myHouse: House = { garden: false, parking: false }
type Room = { size: number } type Room = { height: number } // 識別子 'Room' が重複しています。
クラスの実装
クラスはインターフェース・型エイリアスを実装できる。
複数実装できるし、両者を同時に実装することも可能。
実装が抜けている箇所があれば、TypeScriptがエラーを教えてくれる。
インターフェースと型エイリアスの複数実装の例。
type Human = { run(): void cook(food: string): void } interface Gorilla { power: number } class Watasi implements Human, Gorilla { power = 100 run() { console.log('走る!') } cook(food) { console.log(`${food}を料理`) } }
「インターフェースの実装」と「抽象クラスの拡張」の違い
インターフェースの実装 | 抽象クラスの拡張 | |
---|---|---|
ランタイムコードの発行 | しない(ので軽量) | する(つまりJavaScriptのクラス) |
汎用性 | 高い。形状をモデル化。配列、関数、クラス、クラスインスタンスを表現できる | 低い。クラスのモデル化に特化 |
アクセス修飾子 | 使用不可 | 使用可 |
複数のクラスで実装を共有し、継承させる場合は抽象クラスを、「このクラスはTである」と表現するための軽量な方法が必要な場合はインターフェースを使用する。
ジェネリック
クラスもインターフェースもジェネリックが使える。
クラスの具体例。
class Soccer<T, U> { constructor( ball: T, goal: U ) {} shoot(ball: T, goal: U): void { // ... } static watch<T, U>(game: T, date: U): Soccer<T, U> { return // ... } }
静的メソッドはクラスのジェネリックにアクセスできないので、代わりに独自のジェネリックを宣言している。
インターフェイスの具体例。
interface Baseball<T, U> { hit(ball: T): U }
finalクラスをシミュレートする
TypeScriptはfinalキーワードをサポートしていないが、コンストラクタにprivateを追加することでシミュレート可能。
class BallpointPen { private constructor(private ink: number) {} } class OilBasedBallpointPen extends BallpointPen {} // クラス 'BallpointPen' を拡張できません。Class コンストラクターがプライベートに設定されています。 new BallpointPen(50) // クラス 'BallpointPen' のコンストラクターはプライベートであり、クラス宣言内でのみアクセス可能です。
しかしこれだと、インスタンス化までできなる。そこで専用の静的メソッドを追加する
class BallpointPen { private constructor(private ink: number) {} static create(ink: number) { return new BallpointPen(ink) } } class OilBasedBallpointPen extends BallpointPen {} // クラス 'BallpointPen' を拡張できません。Class コンストラクターがプライベートに設定されています。 BallpointPen.create(50)
今回はここまで。
インターフェースは同じ名前で複数宣言するとマージされちゃうので、exportするときなど気をつけないと。
プリミティブ型やリテラルのユニオン型とかを宣言できるので、型エイリアスの方がインターフェースより使い勝手が良いかもしれない。
次回は「高度な型」についての予定。