ts.svg

【TypeScript】クラスとインターフェース

TypeScript

クラス

クラスについて

JavaScript では、ES6(ES2015)からクラスが導入されています。 基本的には Java や C#などのオブジェクト指向プログラミングで使用されているものと同じです。 クラスがどのようなものなのかは、ここでは言及しません。

クラスは TypeScript でも使用することができ、型定義の恩恵だけでなく、後述するインタフェースなど独自の機能を利用することができます。

定義と宣言

クラスは、以下のようにプロパティとコンストラクタによって定義します。 プロパティは、他の言語ではフィールドやメンバ変数と言われているものです。

class User {
  //プロパティ
  id: number
  name: string
  //コンストラクタ
  constructor(id: number, name: string) {
    this.id = id
    this.name = name
  }
}

定義したクラスは、newを使ってインスタンスを生成します。 インスタンスを生成する際は、事前にクラスを定義しておく必要があります。 関数のように巻き上げ(ホイスティング)は行われません。

//インスタンスの生成
const user: User = new User(1, 'Taro')
//プロパティの参照
console.log(user.id) // 1
console.log(user.name) // Taro

コンストラクタのオーバーロードは、他の言語と違い少し特徴的です。 どのような引数を持つコンストラクタかということと、実際にどのような処理を行うかということを分けて定義します。

class User {
  id: number
  name: string
  //コンストラクタ定義
  constructor(id: number)
  constructor(id: number, name: string)
  //コンストラクタの実体を定義
  constructor(id: number, name?: string) {
    this.id = id
    this.name = name ?? ''
  }
}

//インスタンスの生成
const user1: User = new User(0001)
const user2: User = new User(0001, 'Taro')

メソッド

メソッドは以下のように定義します。 自クラス内でプロパティにアクセスするには、thisが必要になります。

class User {
  id: number
  name: string
  constructor(id: number, name: string) {
    this.id = id
    this.name = name
  }
  //メソッド定義
  hello(): void {
    console.log(`Hello, ${this.name}.`)
  }
}

const user: User = new (1, 'Taro')()
//メソッドの実行
user.hello() // Hello, Taro.

アクセス修飾子

privateprotectedpublicによってプロパティのアクセス制御を行います。

アクセス修飾子説明
private自クラス内でのみアクセス可
protectedサブクラスまでアクセス可
publicどこからでもアクセス可(デフォルト)

アクセス修飾子は TypeScript の機能になります。 通常の JavaScript にはありませんので注意してください。

class MyClass {
  private privateVal: string
  protected protectedVal: string
  public publicVal: string
  //...
}

const myClass: MyClass = new MyClass('A', 'B', 'C')
console.log(myClass.privateVal) //NG
console.log(myClass.protectedVal) //NG
console.log(myClass.publicVal) //OK

コンストラクタの引数にアクセス修飾子を指定することで、プロパティ定義を省略することができます。

class User {
  constructor(public id: number, public name: string) {
    this.id = id
    this.name = name
  }
}

ゲッター・セッター

ゲッターとセッターは、それぞれgetsetを使用して定義します。

class User {
  constructor(private id: number) {
    this.id = id
  }
  //ゲッター
  get myId(): number {
    return this.id
  }
  //セッター
  set myId(id: number) {
    this.id = id
  }
}

const user: User = new User(1)
//セッターの使用
user.myId = 2
//ゲッターの使用
console.log(user.myId) //2

static

static修飾子は、静的なプロパティまたはメソッドを定義します。 これらはクラスのインスタンスからではなく、クラスそのものから参照します。

class Circle {
  static pi: number = 3.14
  static circumference(radius: number): number {
    return 2 * radius * this.pi
  }
}

console.log(Circle.pi) //3.14
console.log(Circle.circumference(2)) //12.56

継承

クラスの継承は、extendsを用いて行います。 継承先のクラスをサブクラス、継承元のクラスをスーパークラスを言います。

//スーパークラス
class User {
  constructor(public id: number, public name: string) {
    this.id = id
    this.name = name
  }
  hello() {
    console.log(`Hello, ${this.name}.`)
  }
}

//サブクラス
class ExUser extends User {
  constructor(id: number, name: string, public age: number) {
    super(id, name)
    this.age = age
  }
  sayAge() {
    console.log(`${this.name} is ${this.age} years old.`)
  }
}

//インスタンスの生成
const user: ExUser = new ExUser(1, 'Taro', 20)
user.hello() //Hello, Taro.
user.sayAge() //Taro is 20 years old.

スーパークラスのプロパティやメソッドはthisで参照でき、コンストラクタはsuper()で使用します。

インターフェース

インターフェースとは

インターフェースは TypeScript で使用できる機能になります。 他の言語と同様に、ポリモーフィズムを目的とした仕組みとなります。 ポリモーフィズムについてここでは言及しません。

TypeScript のインターフェースは、どのようなプロパティやメソッドを持つかをあらかじめ定義するためのもので、 実際にどのような値、処理をするかはクラスなどで実装(implements)します。

少し強引ではありますが、これまで例に出してきたUserクラスをインタフェースを用いて定義してみます。

//インターフェースの定義
interface Management {
  id: number
}
interface Person {
  name: string
  hello(): void
}

//クラス定義
class User implements Management, Person {
  constructor(public id: number, public name: string) {
    this.id = id
    this.name = name
  }
  hello() {
    console.log(`Hello, ${this.name}.`)
  }
}

インターフェースの実装にはimplementsを使用します。 複数ある場合はカンマ区切りで指定します。 インターフェースで指定したプロパティとメソッドは、必ずクラスで定義しなければいけません。

では、次のようにインスタンスを生成してみます。

const user: User = new User(1, 'Taro')
const management: Management = user
const person: Person = user

このとき、managementpersonには以下のようなオブジェクトが代入されます。 実は、これをそのまま宣言すれば同じ結果が得られます。

const management: Management = {
  id: 1
};
const person: Person = {
  name: 'Taro'
  hello() {
    console.log(`Hello, ${this.name}.`);
  };
};

TypeScript におけるインターフェースは、他言語と同様にクラスを実装するためのものではありますが、 オブジェクトがどのようなデータ構造をもつかを定義する用途でも使用できます。

結局のところクラスのインスタンスも以下のようなオブジェクトである考えると、インタフェースがオブジェクトの定義となるのもわかる気がします。 (クラスを以下のように宣言することはできません。)

const user: User = {
  id: 1,
  name: 'Taro',
  hello() {
    console.log(`Hello, ${this.name}.`);
  };
};

定義

既に記述してありますが、インターフェースはinterfaceを使用してオブジェクトを定義します。 定義には、キー(プロパティ名)とその型を指定します。メソッドが必要な場合は、メソッド名、引数とその型、戻り値の型を指定します。

interface User {
  id: number
  name: string
  hello(): void
}

インターフェースを用いてオブジェクトを宣言する場合は、定義したすべてのプロパティ、メソッドを指定する必要があります。 また、定義にないプロパティやメソッドは指定できません。

const user: User = {
  id: 1,
  name: 'Taro',
  hello() {
    console.log(`Hello, ${this.name}.`)
  };
};

継承

インターフェースは、クラス同様にextendsで継承することができます。

interface Management {
  id: number
}
interface Person {
  name: string
  hello(): void
}

interface User extends Management, Person {
  age: number
}

複数継承する場合は、カンマで区切ります。 継承先のインターフェースでは、宣言時に継承元を含めたすべてのプロパティ、メソッドを指定する必要があります。

const user: User = {
  id: 1,
  name: 'Taro',
  age: 20,
  hello() {
    console.log(`Hello, ${this.name}`);
  };
};

ユニオン型

通常の型と同様に、|を使って複数のインタフェースを指定することができます。

interface Management {
  id: number
};
interface Person {
  name: string,
  hello(): void
};

const user: Management | Person = {
  id: 1,
  name: 'Taro',
  hello() {
    console.log(`Hello, ${this.name}`);
  };
};

継承と異なり、いずれか 1 つのインタフェースのプロパティ、メソッドが揃っていればよく、すべてを指定する必要はありません。

const user1: Management | Person = { id: 1 } //OK
const user2: Management | Person = { id: 1, name: 'Taro' } //OK
const user3: Management | Person = { name: 'Taro' } //NG

指定したインタフェースで、型の違う同じプロパティを持つ場合はマージされます。 つまり、どちらの型も持つユニオン型となります。 ただし、上記で説明した通り、いずれか 1 つのインターフェースの条件を満たさなければエラーとなります。

interface A {
  hoge: string
  fuga: number
}
interface B {
  hoge: number
  piyo: string
}
const x: A | B = {
  hoge: 'A',
  fuga: 0,
  piyo: 'B',
}
x.hoge = 1 //OK
const y: A | B = {
  hoge: 'A',
  fuga: 0,
}
y.hoge = 1 //NG: piyoがないためエラー

互換性

前述したように、インターフェースの定義にないプロパティやメソッドは、宣言時に指定することができません。

interface User {
  id: number
  name: string
}

//以下はNG
const user: User = {
  id: 1,
  name: 'Taro',
  age: 20,
}

しかし、宣言済みのオブジェクトからであれば、定義にないプロパティなどがあっても代入が可能です。 ただし、代入されるのは定義されているもののみとなります。

const exUser = {
  id: 1,
  name: 'Taro',
  age: 20,
}

const user: User = exUser //{ id: 1, name: 'Taro` }

再宣言(open ended)

同一名のインタフェースを宣言(再宣言)した場合、その結果はマージされます。 以下を例を参照してください。

interface A {
  hoge: string
  fuga: number
}
interface A {
  piyo: string
}

const x: A = {
  hoge: 'A',
  fuga: 0,
  piyo: 'B',
}

例のように、インターフェースを再宣言すると、これまで定義していたすべてのプロパティを持つものとなります。 同じプロパティを再宣言する場合、型が同じでなければエラーとなります。

interface A {
  hoge: string
  fuga: number
}
interface A {
  hoge: number //NG: 型が違うためエラー
  piyo: string
}