演習問題回答
Typescriptのクラスの特徴
- C# から受け継いだ、Java/Delphiの流れを受け継いだクラスベースのオブジェクト指向
- 継承、インタフェース、アノテーション
- Javascript から展開された、プロトタイプベースのオブジェクト指向
- ミックスイン
の双方の影響を受けている
クラス記法
class Position { //クラスの宣言
constructor (
private x: number, //コンストラクタでの引数宣言と、クラスのメンバ宣言を同時に行うこともできる
private y: number
)
private member: number //メンバ変数の宣言
protected member_protected: string //protected
public member_public: Object //public, デフォルトのアクセス指定子
private readonly member_ro: boolean //readonly
public method(arg: number): string { //メンバー関数
return arg.toString()
}
}
let pos = new Position(100, 200) //x:100, y:200 のPositionをnewする
継承と抽象クラス
extends
キーワードにより継承によるクラスの拡張を行うことができます。
//麻雀牌
class Tiles {
}
//数牌
class Suites extends Tiles {
protected num: number = 1
}
//萬子
class Characters extends Suites {
public printTile(): void {
console.log("")
}
}
let character = new Characters()
本来は、萬子以外の牌がインスタンス化されることはないので、 abstract
キーワードで抽象クラスにします。
//麻雀牌
abstract class Tiles {
}
//数牌
abstract class Suites extends Tiles {
protected num: number = 1
}
//萬子
class Characters extends Suites {
}
let suit = new Suites() // Cannot create an instance of an abstract class.(2511)
super() による親クラスコンストラクタの呼び出し
現在の実装だと、数字が1の数牌しか定義できないので、コンストラクタを追加しましょう。
//麻雀牌
abstract class Tiles {
}
//数牌
abstract class Suites extends Tiles {
constructor(
protected num: number
) {
super() //継承クラスでコンストラクタを定義した場合、親のコンストラクタをsuperで呼び出す必要がある
}
}
//萬子
class Characters extends Suites {
}
let ch = new Characters(1) //明示的にコンストラクタを定義しない場合、暗黙的に親クラスのコンストラクタを利用できる
ついでに赤牌も定義しておきましょう。
class RedCharacters extends Characters {
constructor(){
super(5) //赤牌は5限定なので親クラスコンストラクタをパラメータ付きで呼び出す
}
}
super による親メソッド呼び出し
牌の種別を文字列で出力する関数を考えましょう。
五萬赤牌は、継承元のprintメソッドを使いたいため、superで親メソッドにアクセスしています。
//萬子
class Characters extends Suites {
print(): string{
return `${this.num}萬`
}
}
//五萬赤
class RedCharacters extends Characters {
constructor(){
super(5) //赤牌は5限定なので親クラスコンストラクタをパラメータ付きで呼び出す
}
print(): string{
return `${super.print()} 赤`
}
}
戻り値の型としての this
自分自身と同じものを作成する関数 clone
を考えます。
共通の関数なので、大本の Tiles
に作成することにしましょう。
abstract class Tiles {
public clone():Tiles {
return {...this}
}
}
//数牌
abstract class Suites extends Tiles {
constructor(
protected num: number
) {
super()
}
}
//萬子
class Characters extends Suites {
print(): string{
return `${this.num}萬`
}
}
//五萬赤
class RedCharacters extends Characters {
constructor(){
super(5) //赤牌は5限定なので親クラスコンストラクタをパラメータ付きで呼び出す
}
print(): string{
return `${super.print()} 赤`
}
}
let ch = new Characters(1)
let ch2:Characters = ch.clone() //Type 'Tiles' is missing the following properties from type 'Characters': print, num(2739)
しかしその場合、継承先クラスでも親クラスを返してしまうため、コンパイルエラーとなります。
ここで、thisを用いた型づけをすると、継承先のクラスの型をリターンするようになるため、エラーを発生させずに実装できます。
abstract class Tiles {
public clone():this{
return {...this}
}
}
インタフェース
インタフェースは、実装を持たずに型の宣言のみを持ったクラスです。
interface LogItem {
message: string,
type: number,
print: () => string
}
実際のところ、以下の型エイリアスとかなり似ており、いくつかの違いを除いてほぼ同一に扱うことができます。
type LogItem = {
message: string,
type: number,
print: () => string
}
インタフェースも継承させることができます。
interface UILogItem extends LogItem {
element: Element
}
型エイリアスとインタフェース
型エイリアスとインタフェースは大きく3つの違いがあります。
型エイリアスでしか表現できない型がある
インタフェースは、あくまでにObject型のようにアトリビュートを持った型の構造を規定するものです。
つまり、以下のような構造を持たない型については、interfaceは宣言する術を持ちません。
type numberAlias = number
type Direction = "東" | "南" | "西" | "北"
インタフェースは拡張時に拡張元の型が割り当て可能かを厳密に判断する
以下のようなインタフェース宣言は不可能です。
interface LogItem {
message: string,
type: number,
print: () => string
}
interface UILogItem extends LogItem { //Interface 'UILogItem' incorrectly extends interface 'LogItem'.
print: () => Element
}
なぜなら、拡張元のインタフェース LogItem
が、拡張先のインタフェース UILogItem
に代入できなくなっているからです。
似たようなことを 型エイリアスでやってみます。
type UILogItem2 = LogItem & {print: () => Element}
この場合、コンパイルエラーは発生せず、うまくマージされた型を作ってくれます。
インタフェース宣言は、マージされる
インタフェースは以下のように分割して定義しても、コンパイル時にマージされます。
interface Animal {
name: string
}
interface Animal {
canFly: boolean
}
let animal: Animal = {
name: "Lion",
canFly: false
}
インタフェース宣言の実装
インタフェース宣言は、型エイリアス同様に型定義として利用できますが、クラスに対する制約としても定義できます。
インタフェースは extends
ではなく、 implements
です。
interface Animal {
name: string
}
interface Animal {
canFly: boolean
}
class Cat implements Animal {
constructor(
public name: string,
public canFly: boolean) {
}
}
また、インタフェースは複数を実装することも普通です。
interface Animal {
name: string
canFly: boolean
}
interface Pet {
ownerName: string
nickname: string
}
class Cat implements Animal, Pet {
constructor(
public name: string,
public nickname: string,
public ownerName: string,
public canFly: boolean) {
}
}
この「複数のインタフェースを持てる」という部分が、今後説明する設計パターンを活用する上での重要なポイントになります。
演習問題
麻雀牌のクラスを役牌含めて完成させてください。
継承、インタフェース、抽象クラスなど、今回説明したものは何でも用いてOKです。
(途中実装が資料にありますが、それを無視しても構いません)