この記事は セゾン情報システムズ Advent Calendar 2020 10日目の記事です。
まえがき
TypeScriptにおけるクラスの使い方や機能について書き記しています。
クラスとは
ES2015より追加された機能です。
class構文機能が追加され、JavaScriptでもオブジェクト指向のアプローチがしやすくなりました。
クラスの特徴を大雑把に挙げると・・・
- クラスはオブジェクトの設計図
- クラスから作られたオブジェクトはインスタンスと呼ばれる
- 似たようなオブジェクトを複製するときに便利
といったものが挙げられます。
クラスを定義してオブジェクトを作成
まずはクラスを定義し、インスタンス化します。
nameプロパティ(フィールド)とコンストラクタ関数の引数initNameにstring型
を宣言します。
※コンストラクタ関数はオブジェクトを作成されるときに実行される関数です。
class Hinatazaka {
name: string;
constructor(initName: string) {
this.name = initName;
}
}
const matsuda = new Hinatazaka('松田好花');
console.log(matsuda); // Hinatazaka: {"name": "松田好花"}
クラスにメソッドを追加する
index_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に型定義する
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プロパティを追加しました。
これで求める挨拶が返ってきます。
(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
を使う場合、それはクラス、インスタンス自身を表すのでクラスを型定義します。
これによってthis
はnameプロパティとgretingメソッドを持つオブジェクトという、より厳密な型定義になります。
このことから下記の実行部分でエラーが起こる理由が分かります。
const kageyama = {
name: '影山優佳',
anotherGreeting: matsuda.greeting
}
kageyama.anotherGreeting() // ここでエラー;
これはkageyamaオブジェクトにgreetingメソッドが無いことが原因です。
anotherGreetingをgreetingメソッドに変更します。
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だとそこに型定義が加わるイメージです。
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() { // 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
は継承先を含むクラスの中でのみ読み書き可能になります。
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
は何かの値を代入したい、何かの値を変更したい際に使います。
getter
とsetter
を一緒に使う際は同じ型であることが必要です。
getter
とsetter
を使うことによって各オブジェクトの値の管理がしやすくなります。
getter
継承先でgetter
を定義し、_catchphraseを取得します。
getter
にはget
キーワードを使い関数を定義します。
get
は返り値を指定しないとエラーが起こるので必ず返り値を定義します。
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つ以上の引数が指定しないと以下のエラーが起きるので必ず引数を定義します。
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
は継承にのみ使えるクラスです。
abstract
はabstract
の中でしか使えません。
abstract
クラスは他のクラスの継承元となるベースクラスでインスタンスを生成できません。
継承先でそれぞれ違う内容を定義したい際に有効です。
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つしか作れなくするための方法です。
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
ポイントごとに分解していきます。
private static instance: Member;
instanceというフィールドを定義します。
staticにする理由はstaticメソッド(getInstance)からstaticプロパティを呼び出すためです。privateは外部からアクセスできないのでインスタンスの複製を防ぎます。
型はMemberの型を保持したいということになります。
private constructor(name: string, from: string, public _catchphrase: string) {
super(name, from);
}
コンストラクタ関数にprivate
をつけると外部でnewを使ってインスタンスを作れなくなります。
static getInstance() {
if (Member.instance) { // 2回目以降の処理
return Member.instance;
}
Member.instance = new Member('松田好花', '京都府', '納豆大好き'); // 初回はこの部分が実行される
return Member.instance; // 初回はこの値が返ってくる
}
インスタンスを使わずに外部から使用できるのはstaticメソッドになります。
よって最終的にはこの部分を呼び出し外部で実行します。
この中で行っているのは以下のとおりです。
- 初回は
Member.instance
にインスタンス化したものを代入し、この値を返す。 - 2回目以降は
private static instance: Member
に値があるのでif文の判定が**true
**になりる。
const matsuda = Member.getInstance();
ここで実行します。
結果、いくつインスタンスを作っても同じものが作られるシングルトンパターン
が実現します。
参考までに変数matsuda
の中身は下記の通りです。
Member: {
"name": "松田好花",
"from": "京都府",
"_catchphrase": "納豆大好き"
}
シングルトンパターン
のメリットは動的要因により何度もインスタンス化する際に、正しく動作させるかつメモリの節約になるといった点が挙げられます。
あとがき
以上。TypeScriptにおけるclassについてつらと書いてみました。
TypeScriptに対する心構えは『常に初陣』(大泉氏リスペクト)。
次回はinterface
について書きたいと思います。