• 作成日:

【TypeScript】classで型を使う - JavaScriptとの書き方の違いとは?

【TypeScript】classで型を使う - JavaScriptとの書き方の違いとは?

今回はTypeScriptでclassを使う方法について説明します。

説明する環境は以下です。

  • macOS Catalina v10.15.5
  • Visual Studio Code v1.57.0
  • TypeScript v4.3.5
  • ESLintでエラーチェック
この記事の目次

TypeScriptでclassに型を使う

class構文の使い方はJavaScriptと一緒です。TypeScriptでは引数やメソッドに対して型を指定していきます。

基本的な使い方

以下の例を使って説明します。

class名Animalsを7行目のnewでインスタンス化したときに、「ライオン」というテキストがconstructorメソッドに渡されます。_nameには「ライオン」というstring型が入るので3行目でstring型を指定しています。

2行目にあるnameと型は、4行目で初期化したときに使用するものです。

class Animals {
  name: string;
  constructor(_name: string) {
    this.name = _name; //初期化
  }
}
const animals = new Animals('ライオン');

初期化を省略する方法については後ほど紹介します。

classにメソッドを追加する場合

Animalsクラスにintroメソッドを追加しています。
追加したメソッドは11行目で実行しています。

class Animals {
  name: string;
  constructor(_name: string) {
    this.name = _name;
  }
  intro() {
    console.log(`この動物は${this.name}です`);
  }
}
const animals = new Animals('ライオン');
animals.intro();

class自体に型をつける

class自体に型を指定できます。interfaceやtypeエイリアスを使って定義した型を、implementsでclassに指定します。

interface animals { //typeエイリアスでもOK
  name: string;
  intro: () => void;
}

class Animals implements animals { //ここでimplemensを使って指定
  name: string;
  constructor(_name: string) {
    this.name = _name;
  }
  intro() {
    console.log(`この動物は${this.name}です`);
  }
}
const animals = new Animals('ライオン');
animals.intro();

implementsで指定された型は、最低限classの中にないといけません。animalsというinterfaceにはnameプロパティとintroメソッドの2つの型を定義しています。つまりAnimalsクラスには必ずnameプロパティとintroメソッドがないとエラーになります。プロパティやメソッドがanimalsより多い分にはエラーになりません。

classを使ったときにthisの注意点

オブジェクトの中で、クラスの中にあるメソッドを値として設定した場合、this.nameがundefineになります。

class Animals {
  name: string;
  constructor(_name: string) {
    this.name = _name;
  }
  intro() {
    console.log(`この動物は${this.name}です`);
  }
}
const animals = new Animals("ライオン");

const anotherAnimals = {
  anotherIntro: animals.intro
};
anotherAnimals.anotherIntro(); //この動物はundefineです と表示される

thisは呼び出した場所によって値が変化するため、TypeScriptではそこまで面倒を見てくれません。そこでthisが何かを伝える必要があります。

introメソッドにある第一引数にthisをとります。これはダミーのthisで第一引数にしかとれません。そしてこのthisの型にnameプロパティを指定します。これによって、anotherLion.anotherIntro()を呼び出したときにnameプロパティがないとエラーで表示させることができます。

class Animals {
  name: string;
  constructor(_name: string) {
    this.name = _name;
  }
  intro(this:{ name:string }) {
    console.log(`この動物は${this.name}です`);
  }
}
const animals = new Animals('ライオン');

const anotherAnimals = {
  //name:"シマウマ", //nameプロパティがあればエラーにならない
  anotherIntro: animals.intro,
};
anotherAnimals.anotherIntro(); //anotherLionにはプロパティnameがないよ、というエラー

classを型のように使う

classはnewでインスタンス化されたときに、そのclassの型も一緒に作成します。以下はthisに対してAnimalsの型を指定してます。

この場合thisにはAnimalsという型が指定されているので、nameというプロパティとintorというメソッドを持っていないとエラーになります。

class Animals { //このclassはnameプロパティとintroメソッドの2つを持っている
  name: string;
  constructor(_name: string) {
    this.name = _name;
  }
  intro(this: Animals) { //Animalsという型を指定
    console.log(`この動物は${this.name}です`);
  }
}
const animals = new Animals('ライオン');

const anotherAnimals = {
  name:"シマウマ", //これがないとエラーになる
  intro:animals.intro //これがないとエラーになる
};
anotherAnimals.intro();

private修飾子とpublic修飾子とは?

private修飾子はclassの中ではアクセスできるが、classの外ではアクセスできないようにするもの。public修飾子はデフォルトの指定で、classの中でも外でもアクセスすることができます。

class Animals {
  name: string;
  age:number;
  constructor(_name: string,_age:number) {
    this.name = _name;
    this.age = _age;
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン',12);

//publicの状態だとageの値を外からでも変更できてしまう
animals.age = 50; 

外からアクセスさせたくない場合は、private修飾子をつけます。
プロパティでもメソッドでも指定が可能です。

class Animals {
  private name: string;
  private age: number;
  constructor(_name: string, _age: number) {
    this.name = _name;
    this.age = _age;
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

//animals.age = 50; classの外から変更できない!
//console.log(animals.age); //classの外から参照もできない!

classで初期化の処理を省略する書き方

constructorのパラメーターに必ずprivateやpublicを指定して書くことで、初期化の処理を省略できます。

class Animals {
  constructor(private name: string, private age: number) {
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

readonly修飾子で読み込み専用にする(書き込めない)

readonly修飾子をつけるとclassの中でも外でも、初期化の処理後は参照することはできても、書き込むことができなくなります。また、つける順番としてprivateやpublicのあとにreadonlyを指定する必要があります。(順序が逆になるとエラー)

class Animals {
  constructor(private readonly name: string, private readonly age: number) {
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

readonlyは読み込み専用ですが、constructorの内側はまだ初期化する段階なので、変更することは可能です。

class Animals {
  constructor(private readonly name: string, private readonly age: number) {
    this.name = 'キリン'; //ここはreadonlyになっていても変更できる
    this.age = 90; //ここはreadonlyになっていても変更できる
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

classを継承する

共通する変数だったり、メソッドを持っている場合、classはextendsを使って継承できます。

ZooのclassにAnimalsのclassを継承させているので、Animalsが持っているnameとageは、Zooでもインスタンス化するときに必要になります。(無いとエラーになる)

class Animals {
  constructor(private name: string, private age: number) {
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

class Zoo extends Animals {} //Animalsクラスを継承する

const zoo = new Zoo('さる', 4); //Animalsクラスにはnameとageがあるため、同じように指定する必要がある
zoo.intro(); //継承しているメソッドも使用できる

classを継承しつつ、さらに何かを加えるときは?

他のクラスを継承するだけではなく、さらに何か追加したい場合はsuperをつける必要があります。

Animalsのクラスを継承したZooのconstructorには、必ず継承元の引数を指定する必要です。今回だとnameとageはAnimalsのclassにもあるため、Zooのclassでも指定する。そしてsuper関数の引数にもnameとageを指定すること。そして追加したいものがあれば、constructorの中に追加していきます。

class Animals {
  constructor(private readonly name: string, private readonly age: number) {
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

class Zoo extends Animals {
  constructor(name: string, age: number, public gender: string) { //Animalsにある引数と加えて、追加していく
    super(name, age);
  }
}

const zoo = new Zoo('さる', 4, 'オス');

継承元のメソッドを上書きしたいとき

Animalsにあるintroメソッドで表示させる内容をZooでは変更したい場合、introメソッドを上書きして指定する必要があります。

ただし、以下のように指定するとAnimalsのクラスでnameとageはprivate修飾子を指定しているためエラーになってしまいます。

class Animals {
  constructor(private readonly name: string, private readonly age: number) {
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

class Zoo extends Animals {
  constructor(name: string, age: number, public gender: string) {
    super(name, age);
  }
  intro() {
    //↓nameもageもAnimalsクラスではprivateが指定されているため、エラーになる!
    console.log(`この動物は${this.name}です。年齢は${this.age}です。性別は${this.gender}です。`);
  }
}

const zoo = new Zoo('さる', 4, 'オス');

継承先でもAnimalsクラスの変数を使いたい場合は、private修飾子ではなく、protected修飾子を使います。継承先では使えますが、それ以外では使うことができません。

class Animals {
  constructor(protected readonly name: string, protected readonly age: number) {
  }
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。`);
  }
}
const animals = new Animals('ライオン', 12);

class Zoo extends Animals {
  constructor(name: string, age: number, public gender: string) {
    super(name, age);
  }
  intro() {
    //↓エラーにならない
    console.log(`この動物は${this.name}です。年齢は${this.age}です。性別は${this.gender}です。`);
  }
}

const zoo = new Zoo('さる', 4, 'オス');

classでgetを使う(ゲッター)

getを実行したタイミングで、何かしら処理をさせるもの。
必ずreturnがないとエラーになります

getとsetは同じプロパティ名にすることができますが、型を統一する必要があります。同じ名前なのにgetがstring型に対して、setがnumber型だとエラーになるということです。

class Animals {
	get colorChange() {
	  if (!this.color) { //もしcolorが空なら
		throw new Error('色の指定がありません');
	  } else {
		return this.color;
	  }
	}
	constructor(private name: string, private age: number, private color: string) {}
	intro(this: Animals) {
	  console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
	}
  }
  const animals = new Animals('ライオン', 12, '赤');
  console.log(animals.colorChange);

classでsetを使う(セッター)

setでは最低でも1つの引数を持つ必要があります

getとsetは同じプロパティ名にすることができますが、型を統一する必要があります。同じ名前なのにgetがstring型に対して、setがnumber型だとエラーになるということです。

class Animals {
	set colorChange(value: string) {
	  if (!value) {
		throw new Error('色の指定がありません');
	  } else {
		this.color = value;
	  }
	}
	constructor(private name: string, private age: number, private color: string) {}
	intro(this: Animals) {
	  console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
	}
  }
  const animals = new Animals('ライオン', 12, '赤'); //ここはcolorを赤で指定している
  
  animals.colorChange = '黒'; //色を黒に変更
  animals.intro(); //「この動物はライオンです。年齢は12です。色は黒です。」と表示される

staticを使って、インスタンスを作らずにclassを使う

classで書いた内容をnewすることでインスタンス化され、設定した値を使うことができますが、インスタンス化せずにclassを使う場合はstaticを使います。

class Animals {
  static infomation = 'ここはAnimalsのクラスです';

  constructor(private name: string, private age: number, private color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
}
//new Animalsなくてもclassにアクセスできる
console.log(Animals.infomation); //「ここはAnimalsのクラスです」と表示される

メソッドも同じく作れます。

class Animals {
  static AdultJudge(age: number) {
    if (age > 20) return true;
    return false;
  }
  constructor(private name: string, private age: number, private color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
}
//new Animalsなくてもclassにアクセスできる
console.log(Animals.AdultJudge(30)); //trueと表示される

継承先でもstaticは使える

Animalsのclassを継承しているZooのclassからも、Animalsのclassにあるstaticにアクセスできます。

class Animals {
  static infomation = 'ここはAnimalsのクラスです';
  static AdultJudge(age: number) {
    if (age > 20) return true;
    return false;
  }
  constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
}

class Zoo extends Animals {
  constructor(name: string, age: number, color: string, public gender: string) {
    super(name, age, color);
  }
  intro() {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。性別は${this.gender}です。`);
  }
}

console.log(Animals.infomation); //ここはAnimalsのクラスです
console.log(Animals.AdultJudge(30)); //true
console.log(Zoo.infomation); //ここはAnimalsのクラスです
console.log(Zoo.AdultJudge(30)); //true

classの中からstaticを操作するには?

classの中でstaticにアクセスする場合、staticのメソッドについては、thisでもクラス名でも参照可能です。通常のメソッドではクラス名でしか参照できません。

class Animals {
  static infomation = 'ここはAnimalsのクラスです';
  speach() {
    console.log(Animals.infomation); //クラス名でしか参照できない
  }
  static call(){
    console.log(this.infomation); //thisでも参照できる
    console.log(Animals.infomation); //クラス名でも参照できる
  }
  constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
}

Abstractクラスを使って、継承先のみで使えるclassを作る

classを継承先でしか使えないようにするために、classの先頭にabstractをつける必要があります(1行目)。abstractがついたclassはnewでインスタンス化できません。

以下の例では、継承先(Zooクラス)で書いたメソッドを、継承元(Animalsクラス)で実行させています。

abstract class Animals { //継承元

  constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
    this.call(); //ここで実行している
  }
  abstract call(): void;
}

class Zoo extends Animals { //継承先

  call() { //このメソッドをAnimalsクラス側で実行したい
    console.log(`この子のニックネームは${this._nickName}です`);
  }

  get nickName() {
    if (!this._nickName) {
      throw new Error('この子にニックネームはありません');
    } else {
      return this._nickName;
    }
  }

  constructor(name: string, age: number, color: string, public gender: string, protected _nickName: string) {
    super(name, age, color);
  }
}
const zoo = new Zoo('さる', 4, 'レッド', 'オス', 'アパー');
zoo.intro(); //Zooクラスにはintroメソッドはないが、Animalsクラスから継承しているので使用できる

継承先であるZooクラスにはcallメソッドを定義しておきます。(11行目)
このメソッドを継承元のAnimalクラスで実行するためには、Animalクラスの先頭にabstractと指定します。(1行目)

次にAnimalsクラスの中に確実にcallメソッドがあると認識させるようにabstructとcallメソッドを書きます。(7行目)

あとはAnimalsクラスの中の実行したい場所に、Zooクラスに定義したcallメソッドを指定することができます。(6行目)

インスタンスを1つしか生成させないようにする(シングルトンパターン)

インスタンスを1つだけしか作れないようにして、かつ外部からもアクセスできるようにすることをシングルトンパターンといいます。

まずはインタンスを外からアクセスできないようにconstructorにprivate修飾子を指定します(2行目)。これでclassの外側からnewによるインスタンス化ができなくなりました。

class Animals {
  private constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
}
//const animals = new Animals('ライオン', 12, '赤'); 外部でインスタンス化ができなくなった

次にインスタンスを作らなくてもアクセスできるstaticをつけたgetInstanceメソッドを作り、その中でAnimalsクラスをインスタンス化させます。

class Animals {
  private constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
  static getInstance() {
    const animalsInstance = new Animals('ライオン', 12, '赤');
    return animalsInstance;
  }
}
const animals = Animals.getInstance();

getInstanceメソッドは11行目で実行しています。

class Animals {
  private static instance: Animals;
  
  private constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
  static getInstance() {
    if (Animals.instance) return Animals.instance;
    Animals.instance = new Animals('ライオン', 12, '赤');
    return Animals.instance;
  }
}
const animals = Animals.getInstance();

インスタンスが作成された場合、そのインスランスを保持するためにprivate修飾子とstaticをつけたinstanceプロパティを書いておきます。そのinstanceプロパティ自体はAnimalsクラスを指しているので型もAnimalsを指定します(2行目)

Animalsのインスタンスがすでに作られていれば、いまあるインスタンスを返し、無い場合はインスタンスを作る、という条件分岐を書いています。(9〜11行目)

class Animals {
  private static instance: Animals;
  
  private constructor(protected name: string, protected age: number, protected color: string) {}
  intro(this: Animals) {
    console.log(`この動物は${this.name}です。年齢は${this.age}です。色は${this.color}です。`);
  }
  static getInstance() {
    if (Animals.instance) return Animals.instance;
    Animals.instance = new Animals('ライオン', 12, '赤');
    return Animals.instance;
  }
}
const animals = Animals.getInstance();
animals.info(); //インスタンスにアクセスできる!

これでインスタンスは1つのみ作られ、内部からも外部からもインスタンスにアクセスできるようになります。

さいごに

今回はTypeScriptでclassで型を使う手順について説明しました。classはJavaScriptもありますが、TypeScriptではさらに細かい指定ができるようになります。

TypeScriptの基礎的な型のつけ方については以下を参照してください。

応用編は以下をどうぞ。