LoginSignup
0
0

More than 3 years have passed since last update.

ミックスイン、デコレータ TypeScript & Angular 勉強会 #6

Last updated at Posted at 2020-09-13
1 / 8

クラスとジェネリクス

クラスにもジェネリクスを適用させることができます。
例えば、一般的な辞書型を定義してみましょう。

class Dictionary<K,V>{ //Key と Value の型をそれぞれ指定できるDictionaryとする
  set(key: K, value: V){
    // TBD
  }

  get(key: K): V{
    return null //TBD
  }
}

ところでちょっとおさらいですが、key を特定の型、たとえば文字列と数値、シンボルのみにしぼりたい場合はどうすればいいでしょうか?

こうします。

class Dictionary<K extends number | string | symbol, V>{ //Key と Value の型をそれぞれ指定できるDictionaryとする
  set(key: K, value: V){
    // TBD
  }

  get(key: K): V{
     throw new Error('NOT IMPLIMENTED') //TBD
  }
}

静的メンバ

オブジェクト指向言語ではおなじみの静的(スタティック)メンバも定義可能です。

class Google {
    private static notFound = "Not Found"
    static setNotFound(message: string): void {
        Google.notFound = message
    }
    search(word: string){
        return Google.notFound
    }
}

Google.setNotFound("見つかりません")
let google = new Google()
let result = google.search("bing") //"見つかりません"

クラスで暗黙的に定義されるインタフェース

TypeScript では、「値」と「型」は明確に異なる名前空間で定義されます。
例えば、以下のような宣言はエラーにはなりません。

//値としての宣言
let a = 0
function f(){
    return null
}

//型としての宣言
type a = number
interface f {
    (): void
}

では「クラス」は値でしょうか、型でしょうか。

答えは両方です。

したがって以下は両方エラーです。

class C {
}

type C = "class" //Duplicate identifier 'C'.(2300)
let C = "class" //Duplicate identifier 'C'.(2300)

具体的に言うと、例えば先程のGoogleクラスは、以下のインタフェースを暗黙的に定義し、かつ Google という名前で IGoogleConstructor を実装した値を定義していることになります。

interface Google {
    search: (word: string) => string
}

interface IGoogleConstructor {
    new() : Google
    setNotfound : (message: string) => string
}

// 実際には new は特殊シグネチャなので実装できない(コンパイルエラー)
const Google: IGoogleConstructor = {
    setNotfound: () => "",
    new: () => {
        return {
            search: () => ""
        }
    }
}

ミックスイン

ミックスインとは、Scalaなどで見られる多重継承をうまく扱う方法です。Scalaでのミックスイン実装を見てみましょう。

abstract class A {
  val message: String
}
class B extends A {
  val message = "I'm an instance of class B"
}
trait C extends A {
  def loudMessage = message.toUpperCase()
}
class D extends B with C

val d = new D
println(d.message)  // I'm an instance of class B
println(d.loudMessage)  // I'M AN INSTANCE OF CLASS B

TypescriptにはScalaのtraitのように、ミックスインのための機能はありませんが、ミックスインのように既存のクラスの機能を拡張することはできます。


Typescriptでのミックスイン実現

以下のように、任意のクラスに対してデバッグ文字出力を行う方法を考えます。

class User {
// ... 
}

let user = new User(3, "Taro Yamada")

user.debug() // 'User({"id": 3, "name": "Taro Yamada"})' 

このように、特定のクラスに対して debug インタフェースを追加するようなミックスイン withEZDebug を考えます。

type ClassConstructor = new(...args: any[]) => {} 

function withEZDebug<C extends ClassConstructor>(Class: C) { 
  return class extends Class {
    constructor(...args: any[]) {
      super(...args)
    }
  } 
}

ClassConstructor は任意の引数を持つコンストラクタを持つクラス、すなわちあらゆるクラスのコンストラクタを指すインタフェースです。

つまり、 withEZDebug には、あらゆるクラスのコンストラクタを引数にわたすことができます。先ほど説明したように、クラスの宣言では暗黙的にクラス自体の型と、そのクラスを生成するコンストラクタの値が生成されます。そのうちの後者を引数に取ることができるので、withEZDebug はクラス宣言自体を引数に取ることができます。

class User {
// ... 
}

let DebuggableUser = withEZDebug(User)
let user = new DebuggableUser(3, "Taro Yamada")

User.debug() // 'User({"id": 3, "name": "Taro Yamada"})' 

あとは、コンストラクタで返すクラスに、メソッドの実装を行いましょう。

type ClassConstructor = new (...args: any[]) => {}

function withEZDebug<C extends ClassConstructor>(Class: C) {
    return class extends Class {
        constructor(...args: any[]) {
            super(...args)
        }

        debug() {
            let Name = this.constructor.name
            return Name
        }
    }
}

class User {
    constructor(
        private id: number,
        private name: string) { }
}

let DebuggableUser = withEZDebug(User)
let user = new DebuggableUser(3, "Taro Yamada")

console.log(user.debug())

しかし、コンストラクタ名だけだと、デバッグとしては不十分です。各クラスに getDebugValue というメソッドが実装されているクラスだけが拡張でき、かつそのメソッドの中身を実行するように型で束縛しましょう。

type ClassConstructor<T> = new (...args: any[]) => T

function withEZDebug<C extends ClassConstructor<{getDebugValue: () => string}>>(Class: C) {
    return class extends Class {
        constructor(...args: any[]) {
            super(...args)
        }

        debug() {
            let Name = this.constructor.name
            let value = this.getDebugValue()
            return Name + '(' + value + ')'
        }
    }
}

class User {
    constructor(
        private id: number,
        private name: string) { }

    getDebugValue(){
        return JSON.stringify(this)
    }
}

let DebuggableUser = withEZDebug(User)
let user = new DebuggableUser(3, "Taro Yamada")

console.log(user.debug())

デコレータ(実験的機能)

ミックスインは便利な機能ですが、Typescriptではもう少し直感的に上記の実装を行う機能としてデコレータというものが用意されています(Javaでいうアノテーション、C#でいうアトリビュート、Pythonのデコレータと同様です)

上記のクラス拡張を、クラスの定義にデコレータとして以下のように書くことで実現できます。

@withEZDebug
class DecoratedUser {
    constructor(
        private id: number,
        private name: string) { }

    getDebugValue() {
        return JSON.stringify(this)
    }
}

//残念ながらany型キャストが必要
let decolatedUser:any = new DecoratedUser(4, "Ichiro Suzuki")
console.log(decolatedUser.debug())

ただし、Typescriptコンパイラとしては残念なことに型にメソッドがデコレートされたことまでは解釈してくれません。従って、型にメソッドを増やすような用途で利用することはできません。


演習問題

本文中で示したScalaのtraitをTypescriptで書き換えてみてください。

0
0
0

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
  3. You can use dark theme
What you can do with signing up
0
0