LoginSignup
4

More than 1 year has passed since last update.

posted at

updated at

Organization

最高にトゥースなTypeScript class編

この記事は セゾン情報システムズ Advent Calendar 2020 10日目の記事です。

まえがき

TypeScriptにおけるクラスの使い方や機能について書き記しています。

クラスとは

ES2015より追加された機能です。
class構文機能が追加され、JavaScriptでもオブジェクト指向のアプローチがしやすくなりました。
クラスの特徴を大雑把に挙げると・・・

  • クラスはオブジェクトの設計図
  • クラスから作られたオブジェクトはインスタンスと呼ばれる
  • 似たようなオブジェクトを複製するときに便利

といったものが挙げられます。

クラスを定義してオブジェクトを作成

まずはクラスを定義し、インスタンス化します。
nameプロパティ(フィールド)とコンストラクタ関数の引数initNameにstring型を宣言します。
※コンストラクタ関数はオブジェクトを作成されるときに実行される関数です。

index_1.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }
}

const matsuda = new Hinatazaka('松田好花');
console.log(matsuda); // Hinatazaka: {"name": "松田好花"} 

クラスにメソッドを追加する

index_1.tsにメソッドを追加します。

index_2-1.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }

  // メソッド
  greeting() {
    console.log(`日向坂46の${this.name}です!`);
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.greeting(); // 日向坂46の松田好花です!

さらに下記のオブジェクトを追加し、greetingメソッドを実行します。

const kageyama = {
  anotherGreeting: matsuda.greeting // (property) anotherGreeting: () => void
}

kageyama.anotherGreeting(); //日向坂46のundefinedです!

名前の部分がudefinedになってしまいましたが、これはthis.nameが原因です。
オブジェクトの中のメソッドでthisを使うと、そのthisはオブジェクト自身を表します。
つまり「kageyamaオブジェクト内にthisが未定義です!」という状態です。
これを解決するにはkageyamaオブジェクト内にもnameプロパティが必要になります。
加えてこのような間接的な形でメソッドを実行するとエラーは出ないですが、thisが何を定義しているのかTypeScriptは推測できません。
よってthisを型定義して明確にしてあげる必要があります。

thisに型定義する

index_2-2.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }

  greeting(this: {name: string}) { // 引数にthisの型定義を設定
    console.log(`日向坂46の${this.name}です!`);
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.greeting(); // 日向坂46の松田好花です!

const kageyama = {
  name: '影山優佳', // nameプロパティを定義
  anotherGreeting: matsuda.greeting
}

kageyama.anotherGreeting(); // 日向坂46の影山優佳です!

greetingメソッドの引数にthis: {name: string}という型定義とkageyamaオブジェクト内にnameプロパティを追加しました。
これで求める挨拶が返ってきます。

anotherGreetingの型
(property) anotherGreeting: (this: {
    name: string;
}) => void

anotherGreetingの型は上記の通りになっており、thisが型定義されているのが分かります。
このようにthisをインスタンスで使用するには必要な場所(この例ではメソッド)でthisを型定義する必要があります。

クラスを型として使う

TypeScriptにおけるクラスはオブジェクトの設計図という役割に加えて、そのクラスが作り出したインスタンスを表す型も同時に作っています
よってクラスを型として使うことができます。
2のindex_2-2.tsに変更を加えて見ていきます。まず、greetingメソッドの引数の型定義を変更します。

  greeting(this: Hinatazaka) { // thisの型定義をHinatazakaに変更
    console.log(`日向坂46の${this.name}です!`);
  }

クラス、インスタンス内でthisを使う場合、それはクラス、インスタンス自身を表すのでクラスを型定義します。
これによってthisnameプロパティとgretingメソッドを持つオブジェクトという、より厳密な型定義になります。
このことから下記の実行部分でエラーが起こる理由が分かります。

const kageyama = {
  name: '影山優佳',
  anotherGreeting: matsuda.greeting
}

kageyama.anotherGreeting() // ここでエラー; 

これはkageyamaオブジェクトにgreetingメソッドが無いことが原因です。
anotherGreetingをgreetingメソッドに変更します。

index_3.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }

  greeting(this: Hinatazaka) { // thisの型定義をHinatazakaに変更
    console.log(`日向坂46の${this.name}です!`);
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.greeting(); // 日向坂46の松田好花です!

const kageyama = {
  name: '影山優佳',
  greeting: matsuda.greeting // greetingメソッドに変更
}

kageyama.greeting(); // 日向坂46の影山優佳です!

これでクラスを型定義に使いつつ無事に実行できます。
クラスを型定義に使うことによって、より厳密・安全になると共にドキュメント的な役割を果たすメリットもあります。

public、readonly、private修飾子

特定の修飾子を使うことによってアクセスを制限することができます。

public修飾子

デフォルトの設定になります。何も記述が無い場合はpublicが適応されます。

readonly修飾子

readonlyを使うとそのクラスの中と外での読み込み専用になり、書き込みができなくなります。

class Hinatazaka {
  readonly name: string; // readonlyを宣言
  constructor(initName: string) {
    this.name = initName;
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.name = '影山優佳'; // Cannot assign to 'name' because it is a read-only property.

ただし、readonlyはコンストラクタ関数の中だと書き換え可能になります。

class Hinatazaka {
  readonly name: string = '松田好花'; // readonlyを宣言
  constructor() {
    this.name = '影山優佳'; // 書き換え処理
  }
}

new Hinatazaka().name; // 影山優佳

これはコンストラクタ関数がが初期化の処理を行うことに起因します。

private修飾子

privateを使うとそのクラスの中でのみ読み書き可能になり、クラスの外から読み書きできなくなります。

class Hinatazaka {
  private name: string; // privateを宣言
  constructor(initName: string) {
    this.name = initName;
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.name; // Property 'name' is private and only accessible within class 'Hinatazaka'.

初期化の処理を省略する

フィールドでプロパティを宣言しコンストラクタ関数内で初期化する記述は、コンストラクタ関数の引数でpublic・readonly・private修飾子のいずれかを使いプロパティを定義することによって初期化の処理を省略することができます。

class Hinatazaka {
  constructor(public name: string) {} // 引数にpublicを宣言し、プロパティを定義
}

const matsuda = new Hinatazaka('松田好花');
matsuda.name; // 松田好花

フィールドやコンストラクタ関数内の記述が必要なくなるので、すっきりし可読性が良くなります。

extends

extendsはあるクラスの定義を他のクラスに継承するために使います。
使い方などはES2015のそれと同じです。TypeScriptだとそこに型定義が加わるイメージです。

index_4-1.ts
class Hinatazaka {
  constructor(public name: string, public catchphrase: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.catchphrase} 日向坂46の${this.name}です!`);
  }
}

class Member extends Hinatazaka { // extendsを使用しHinatazakaクラスを継承
  constructor(name: string, catchphrase: string, public from: string) { // fromプロパティを追加
    super(name, catchphrase); // super関数を使い継承元のプロパティを定義
  }

  greeting() { // メソッドの上書き
    console.log(`${this.catchphrase} ${this.from}出身、日向坂46の${this.name}です!`);
  }
}

const matsuda = new Member('松田好花', '納豆大好き', '京都府');
matsuda.greeting(); // 納豆大好き 京都府出身、日向坂46の松田好花です!

ポイントは以下の通りです。

  • コンストラクタ関数を継承するにはextendsをしたクラスにコンストラクタ関数を書く。
  • プロパティを追加する際はこのコンストラクタ関数の引数に追加します。上記だとpublic from: stringが該当。
  • 継承先でコンストラクタ関数を使う際は必ずsuper関数を使い、引数には継承元のプロパティを定義。
  • メソッドを上書きしたい際は改めてメソッドを書く。

protect修飾子

private は継承された先でもprivateであり続けます。
そういう際はprotectedを使います。
index_4-1.tsに変更を加えて確認します。

継承元のコンストラクタ関数
constructor(public name: string, private catchphrase: string) {} // catchphraseをprivateに変更
継承先のgreetingメソッド
  greeting() { // Property 'catchphrase' is private and only accessible within class 'Hinatazaka'.
    console.log(`${this.catchphrase} ${this.from}出身、日向坂46の${this.name}です!`);
  }

上記の通り、継承元のコンストラクタ関数の引数catchphraseをprivateに変更すると継承先の継承先のgreetingメソッドでエラーが起きます。先述したようにprivate は継承された先でもprivateであり続けるためです。そこでprotectedを使います。protectedは継承先を含むクラスの中でのみ読み書き可能になります。

index_4-2.ts
class Hinatazaka {
  constructor(public name: string, protected catchphrase: string) {} // privateをprotectedに変更

  greeting(this: Hinatazaka) {
    console.log(`${this.catchphrase} 日向坂46の${this.name}です!`);
  }
}

class Member extends Hinatazaka {
  constructor(name: string, catchphrase: string, public from: string) {
    super(name, catchphrase);
  }

  greeting() { // エラーが出ない
    console.log(`${this.catchphrase} ${this.from}出身、日向坂46の${this.name}です!`);
  }
}

const matsuda = new Member('松田好花', '納豆大好き', '京都府');
matsuda.greeting(); // 納豆大好き 京都府出身、日向坂46の松田好花です!

変更点はprivateの部分をprotectedにしただけです。
これで継承先のエラーが解消されました。

getterとsetter

getterは何かを取得したい、何かの関数を実行したい際に使います。
setterは何かの値を代入したい、何かの値を変更したい際に使います。
gettersetterを一緒に使う際は同じ型であることが必要です。
gettersetterを使うことによって各オブジェクトの値の管理がしやすくなります。

getter

継承先でgetterを定義し、_catchphraseを取得します。
getterにはgetキーワードを使い関数を定義します。
getは返り値を指定しないとエラーが起こるので必ず返り値を定義します

index_5-1.ts
class Hinatazaka {
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です!`);
  }
}

class Member extends Hinatazaka {
  get catchphrase() { // getキーワードを使いcatchphrase関数を宣言
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");
    }
    return this._catchphrase // 値を返却
  }

  constructor(name: string, from: string, public _catchphrase: string) {
    super(name, from);
  }

  greeting() {
    console.log(`${this._catchphrase} ${this.from}出身、日向坂46の${this.name}です!`);
  }
}

const matsuda = new Member('松田好花', '京都府', '納豆大好き');
matsuda.greeting(); // 納豆大好き 京都府出身、日向坂46の松田好花です!

setter

index_5-1.tsにsetterを追加します。
継承先でsetterを定義し、_catchphraseの値を変更します。
setterにはsetキーワードを使い関数を定義します。
set は必ず1つ以上の引数が指定しないと以下のエラーが起きるので必ず引数を定義します

index_5-2.ts
class Hinatazaka {
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です!`);
  }
}

class Member extends Hinatazaka {
  get catchphrase() {
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");

    }
    return this._catchphrase
  }

  set catchphrase(value) { // setキーワードを使いcatchphrase関数を宣言
    if (!value) {
      throw new Error('There is no subject')
    }
    this._catchphrase = value; // 値を代入
  }

  constructor(name: string, from: string, public _catchphrase: string) {
    super(name, from);
  }

  greeting() {
    console.log(`${this._catchphrase} ${this.from}出身、日向坂46の${this.name}です!`);
  }
}

const matsuda = new Member('松田好花', '京都府', '納豆大好き');
matsuda.catchphrase = 'やっほっす~'; // ここで実行、右辺の値がset catchphrase関数の引数になる
matsuda.greeting(); // やっほっす~ 京都府出身、日向坂46の松田好花です!

setterの代入が行われないとgetterの値(new Memberの3番目の引数)が返ってきます。
setterの代入が行われるとその値(matsuda.catchphrase = 'やっほっす~')が返ってきます。

static

staticはクラスに静的メソッドや静的プロパティを定義します。
staticを定義するとインスタンスを作らずににクラスを使うことができます。
staticの宣言は static~の形で書きます。

class Hinatazaka {
  static memberName = '松田好花'; // staticを宣言
}

Hinatazaka.memberName; // 松田好花

インスタンス化せずにmemberNameにアクセスできることが分かります。

abstract

abstractは継承にのみ使えるクラスです。
abstractabstractの中でしか使えません。
abstractクラスは他のクラスの継承元となるベースクラスでインスタンスを生成できません。
継承先でそれぞれ違う内容を定義したい際に有効です。

index_6.ts
abstract class Hinatazaka { // abstractを宣言
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です!`);
    this.memberCatchphrase(); // abstractで定義したメソッドを実行
  }

  abstract memberCatchphrase(): void // メソッド abstractはabstractの中でしか使えない
}

class Member extends Hinatazaka {
  memberCatchphrase() { // 継承元のabstractの機能を定義
    console.log(`キャッチフレーズは${this._catchphrase}`);
  }

  get catchphrase() {
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");

    }
    return this._catchphrase
  }

  constructor(name: string, from: string, public _catchphrase: string) {
    super(name, from);
  }
}

const matsuda = new Member('松田好花', '京都府', '納豆大好き');
matsuda.greeting(); 
// 京都府出身、日向坂46の松田好花です!
// キャッチフレーズは納豆大好き

ポイントは以下の通りです。

  • 継承元のクラスにabstractを宣言。
  • 継承元のクラス内で更にabstractを宣言してメソッドを定義。
  • 継承元のクラス内でabstractメソッド(memberCatchphrase)を実行(上記だとgreetingメソッド内)。
  • 継承先でmemberCatchphraseメソッドの機能を定義。

このように継承を前提に継承先で詳細な機能を定義できるので、複数のクラスを使う際の可読性を担保すると共に拡張性も向上します。

シングルトンパターン

最後にこれまでの要素を応用してシングルトンパターンを実践します。
index_6.tsに変更を加えて確認します。
シングルトンパターンはクラスからインスタンスを1つしか作れなくするための方法です。

index_7.ts
abstract class Hinatazaka {
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です!`);
    this.memberCatchphrase();
  }

  abstract memberCatchphrase(): void
}

class Member extends Hinatazaka {
  private static instance: Member; // ポイント1
  memberCatchphrase() {
    console.log(`キャッチフレーズは${this._catchphrase}`);
  }

  get catchphrase() {
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");

    }
    return this._catchphrase
  }

  private constructor(name: string, from: string, public _catchphrase: string) { // ポイント2
    super(name, from);
  }

  static getInstance() { // ポイント3
    if (Member.instance) {
      return Member.instance;
    }
    Member.instance = new Member('松田好花', '京都府', '納豆大好き');
    return Member.instance;
  }
}

const matsuda = Member.getInstance(); // ポイント4
const konoka = Member.getInstance();
matsuda === konoka // true

ポイントごとに分解していきます。

ポイント1
private static instance: Member;

instanceというフィールドを定義します。
staticにする理由はstaticメソッド(getInstance)からstaticプロパティを呼び出すためです。privateは外部からアクセスできないのでインスタンスの複製を防ぎます。
型はMemberの型を保持したいということになります。

ポイント2
private constructor(name: string, from: string, public _catchphrase: string) {
  super(name, from);
}

コンストラクタ関数にprivateをつけると外部でnewを使ってインスタンスを作れなくなります。

ポイント3
static getInstance() {
  if (Member.instance) { // 2回目以降の処理
    return Member.instance; 
  }
  Member.instance = new Member('松田好花', '京都府', '納豆大好き'); // 初回はこの部分が実行される
  return Member.instance; // 初回はこの値が返ってくる
}

インスタンスを使わずに外部から使用できるのはstaticメソッドになります。
よって最終的にはこの部分を呼び出し外部で実行します。
この中で行っているのは以下のとおりです。

  1. 初回はMember.instanceにインスタンス化したものを代入し、この値を返す。
  2. 2回目以降はprivate static instance: Memberに値があるのでif文の判定がtrueになりる。
ポイント4
const matsuda = Member.getInstance();

ここで実行します。
結果、いくつインスタンスを作っても同じものが作られるシングルトンパターンが実現します。
参考までに変数matsudaの中身は下記の通りです。

Member: {
  "name": "松田好花",
  "from": "京都府",
  "_catchphrase": "納豆大好き"
} 

シングルトンパターンのメリットは動的要因により何度もインスタンス化する際に、正しく動作させるかつメモリの節約になるといった点が挙げられます。

あとがき

以上。TypeScriptにおけるclassについてつらと書いてみました。
TypeScriptに対する心構えは『常に初陣』(大泉氏リスペクト)。
次回はinterfaceについて書きたいと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4