vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

TypeScript で関数オーバーロード

はじめに

C# だと時々使う関数のオーバーロード

TypeScript で同じように書くとエラーになるため、使えないのかな~と思っていたのですが、よくよく調べてみると使えるらしい。しかしながらちょっと様子が違う?となったので、あれこれ試してみることにしました。

オーバーロード

まず C#オーバーロードから。

Sample.cs

public class Sample
{
    public string GetMessage(string message)
    {
        return "hello";
    }
    public int GetMessage(int message)
    {
        return 200;
    }
}

各メソッドはメソッド名と引数の型、数で区別され、別物として扱われます。

戻り値のみ異なる場合はエラーになります。

このコードをそのまま TypeScript で書いてみると。。。

Sample.ts

class Sample{
    public getMessage(message: string): string{
        return message;
    }
    
    // エラーとなる.
    public getMessage(message: number): number{
        return message;
    }
    
}

Duplicate function implementation と怒られてしまいます。

C# に合わせてクラスのメソッドを見ていますが、関数の場合も同じです。

理由は返還後の JavaScript を見ればすぐにわかります。

Sample.js

class Sample {
    getMessage(message) {
        return message;
    }
    getMessage(message) {
        return message;
    }
}

型の情報が消えてしまうため、まったく同じメソッドになってしまう、というわけですね。

ただ、引数の数を変更しても同じようにエラーとなるのは気になるところですが。。。

プロパティなどと同様、動的に変わるものということで、メソッドを判別する要素として使用していない、ということなのでしょう。

とはいえ TypeScript にもオーバーロードはあります

では TypeScript にはオーバーロードはないのか?というと、そうではありません。

Sample.ts

class Sample{
    public getMessage(message: string): string;
    public getMessage(message: number): number;
    public getMessage(message: any): any{
        return message;
    }
}

※ ググった内容から、昔は戻り値が同じ型である必要があったようですが、少なくとも ver.3.6 では異なっていても問題ありません。

最初見たときちょっと驚きましたが、重要なのは実際の処理を行う引数・戻り値が any のメソッドです。

今回はオーバーロードしているメソッドの戻り値が string と number であるため any としていますが、下記のように、オーバーロードしている全メソッドの戻り値の型の親となる型であれば OK です。

Sample.ts

class Sample{
    public getMessage(message: string): "hello";
    public getMessage(message: number): "world";
    // 戻り値は string で OK
    public getMessage(message: any): string{
        return message;
    }
}

なお、 JavaScript に変換した結果は下記の通りです。

Sample.js

class Sample {
    getMessage(message) {
        return message;
    }
}

実際の処理を書いているメソッド以外のオーバーロードの内容はすべて消されてしまうわけですね。

引数の型によって処理を変えたいときもあるよね

やったね TypeScript でもオーバーロードができた!

。。。と言いたいところですが、これだと型が違っても全く同じ処理をすることしかできません。 (全く違うことをするのであれば別のメソッド・関数にすべきだとは思いますが)

となると、いったん引数を any で受け取って、その型を調べて処理を分ける、という方法をとる必要がありそうです。

型を調べる

引数の型に合わせて処理を分けるには、当然ながらその引数の値の型を調べる必要があります。

typeof

string や oject などの基本型は、 typeof(引数) で調べることができます。

  • string
  • number
  • bigint
  • boolean
  • symbol
  • undefined
  • object
  • function

Sample.ts

class Sample{
    public getMessage(message: string): string;
    public getMessage(message: number): number;
    public getMessage(message: any): any{
        if(typeof(message) === "string"){
            console.log("message type is string: " + message);
        }
        if(typeof(message) === "number"){
            console.log("message type is number: " + message);
        }
        return message;
    }
}
let s = new Sample();
s.getMessage("hello");
s.getMessage(30);

出力結果は下記の通りです。

message type is string: hello
message type is number: 30

自作の型・interface・クラス・関数

自作の型・interface・クラス・関数を調べたい場合はどうすればよいでしょうか。

typeof の結果は object となります。

Sample.ts

let s = new Sample();
// object と出力される
console.log(typeof(s));
// コンパイルエラー
console.log(typeof(s) === "Sample");

というわけで、 typeof を使用することはできません。

ではどうするか。

上記ドキュメントによると、このようにして比較するようです。

Sample.ts

type TypeSample = {
    id: number,
    name: string,
};
class Sample{
    private isType(message: any): message is TypeSample{
        return (message as TypeSample).id !== undefined;
    }
}

えぇ。。。

as

ちなみに message as TypeSample だけじゃダメなの?というと。。。

let id = 30;
let t = (id as any as TypeSample);
console.log(t);

一旦 any にキャストしているのは number のままだとコンパイルエラーになるためです。

この出力結果は下記の通り。

30

えぇ。。。 undefined とかじゃないの。。。

実は JavaScript に変換すると as ~ が削除されるため、下記のようになります。

let id = 30;
let t = id;
console.log(t);

そりゃ undefined にはならないですよね~/(^o^)\

あと、引数が指定の型( TypeSample )である、という表現が、 message is TypeSample となるのは面白いですね :)

プロパティ選びは慎重に

この判別方法の課題は、同じプロパティを持っていると区別できない、という点です。

Sample.ts

type TypeSample = {
    id: number,
    name: string,
};
class Sample{
    public getMessage(message: TypeSample): TypeSample;
    public getMessage(message: Function): Function;
    public getMessage(message: any): any{
        console.log(this.isType(message));
    }
    private isType(message: any): message is TypeSample{
        return (message as TypeSample).name != null;
    }
}
let s = new Sample();

// 1. TypeSample を渡した場合.
s.getMessage({
    id: 20,
    name: "たこ焼き"
});

// 2. Function を渡した場合.
s.getMessage(() => console.log("チーズ"));

この実行結果は両方とも true になります。

これは、関数がデフォルトで name というプロパティを持っているためです。

とすると、

Sample.ts

type TypeSample = {
    id: number,
    name: string,
    type: "TypeSample"
};
const TypeNameTypeSample: "TypeSample" = "TypeSample";
class Sample{
    ~省略~
    public getMessage(message: any): any{
        console.log(this.isType(message));
    }
    private isType(message: any): message is TypeSample{
        return (message as TypeSample).type === TypeNameTypeSample;
    }
}
let s = new Sample();

s.getMessage({
    id: 20,
    name: "たこ焼き",
    type: "TypeSample"
});
s.getMessage(() => console.log("チーズ"));

。。。なんだろうこの敗北感。

まぁ string 型にするよりはマシとはいえ、面倒くさいし間違いやすいし。

そもそも判別しうるのか

たとえば type のインスタンスを作って中身を覗いてみると。。。

let t: TypeSample = {
    id: 20,
    name: "たこ焼き"
};
console.log(t);

中身はこのようなものになります。

{…}
    id: 20
    name: "たこ焼き"
    < prototype>: {…}
        __defineGetter__: function __defineGetter__()
        __defineSetter__: function __defineSetter__()
        __lookupGetter__: function __lookupGetter__()
        __lookupSetter__: function __lookupSetter__()
        __proto__: 
        constructor: function Object()
        hasOwnProperty: function hasOwnProperty()
        isPrototypeOf: function isPrototypeOf()
        propertyIsEnumerable: function propertyIsEnumerable()
        toLocaleString: function toLocaleString()
        toSource: function toSource()
        toString: function toString()
        valueOf: function valueOf()
        < get __proto__()>: function __proto__()
        < set __proto__()>: function __proto__()

はい、「 TypeSample 」という型情報がありませんね。

type 、 interface は、例えばあるクラスインスタンスが同じプロパティを持っていれば、その type 、 interface を実装していると見なされます。

そのため、 TypeSample 型であるか、ということは、プロパティからしか判別できないと。

もしプロパティ一つだと判別できない、という話であれば、全プロパティを確認しても良いかもしれません(あまりそのシチュエーションはなさそうですが)。

Sample.ts

~省略~
const TypeSampleChecker: TypeSample = {
    id: -1,
    name: ""
};
class Sample{
    ~省略~
    public getMessage(message: any): any{
        console.log(this.isType(message));
    }
    private isType(message: any): message is TypeSample{
        // TypeSample の全プロパティをチェック.
        for(let p in TypeSampleChecker){
            if(message[p] == null){
                return false;
            }
        }
        return true;
    }
}
let s = new Sample();
s.getMessage({
    id: 20,
    name: "たこ焼き"
});
s.getMessage(() => console.log("チーズ"));

ではクラスインスタンスはどうか

ではクラスインスタンスの場合はどうかというと、 instanceof で判定できそうです。

Sample.ts

~省略~
class ClassSample{
    public id: number = 99;
    public name: string = "めんたい";
}
class Sample{
    public getMessage(message: ClassSample): ClassSample;
    ~省略~
    public getMessage(message: any): any{
        console.log(this.isClass(message));
    }
    ~省略~
    private isClass(message: any): message is ClassSample{
        return message instanceof ClassSample;
    }
}
let s = new Sample();
s.getMessage({
    id: 20,
    name: "たこ焼き"
});
s.getMessage(() => console.log("チーズ"));
let f = () => console.log("コーンポタージュ");

s.getMessage(new ClassSample());

結果は下記の通り。

false
false
true

良いですね :)

ClassSample を継承した場合はどうでしょうか。

Sample.ts

~省略~
class ExtendClass extends ClassSample{
}
s.getMessage(new ExtendClass());

これも true になりました。

もし継承したクラスを除外したい場合、 message.constructor.name でクラス名を取ると良いかもしれません。

念のためプロパティが同じ別のクラスも見ておきます。

Sample.ts

~省略~
class OtherClass{
    public id: number = 99;
    public name: string = "めんたい";
}
s.getMessage(new OtherClass());

結果は false になります。

結果自体は良いのですが、メソッド(関数)の引数としては、クラスが異なっても同じプロパティを持っていれば渡すことができてしまうのですね。

地味に驚きました。

まとめ

これらを組み合わせると、オーバーロードしたメソッドの処理を引数の型に合わせて切り替えられそうです。

Sample.ts

type TypeSample = {
    id: number,
    name: string,
};
class ClassSample{
    public id: number = 99;
    public name: string = "めんたい";
}
const TypeSampleChecker: TypeSample = {
    id: -1,
    name: ""
};
const ClassSampleChecker = new ClassSample();

class Sample{
    public getMessage(message: string): string;
    public getMessage(message: number): number;
    public getMessage(message: ClassSample): ClassSample;
    public getMessage(message: TypeSample): TypeSample;
    public getMessage(message: Function): Function;
    public getMessage(message: any): any{
        switch(typeof(message)){
            case "string":
                console.log("[type:string] " + message);
                break;
            case "number":
                console.log("[type:number] " + message);
                break;
            case "function":
                console.log("[type:function] " + message);
                break;
            case "object":
                // type、classの判別.
                if(this.isClass(message)){
                    console.log("[type:ClassSample] " + message);
                }
                else if(this.isType(message)){
                    console.log("[type:TypeSample] " + message);
                }
                else{
                    console.error("type not found");
                }
                break;
            default:
                console.error("type not found");
                break;
        }
        return message;
    }
    private isType(message: any): message is TypeSample{
        // TypeSample の全プロパティをチェック.
        for(let p in TypeSampleChecker){
            if(message[p] == null){
                return false;
            }
        }
        return true;
    }
    private isClass(message: any): message is ClassSample{
        return message instanceof ClassSample;
    }
}
let s = new Sample();
// string
s.getMessage("サラミ");
// number
s.getMessage(97);
// ClassSample
s.getMessage(new ClassSample());
// TypeSample
s.getMessage({
    id: 20,
    name: "たこ焼き"
});
// Function
s.getMessage(() => console.log("チーズ"));

結果は下記の通り。

[type:string] サラミ
[type:number] 97
[type:ClassSample] [object Object]
[type:TypeSample] [object Object]
[type:function] () => console.log("チーズ")

。。。動作には問題がないのですが、どうでしょうこの溢れ出る別メソッドに分けろよ感は。

ご利用は計画的に( ˘ω˘ )

WSL の Ubuntu 18.04 で SpiderMonkey をビルドした話

はじめに

ここまで TypeScript(JavaScript) の prototype などを追ってきて、ふと思ったのが、「これって内部的にはどう扱われているのだろう」ということでした。

そこで、 JavaScript を動かすのに使われる JavaScript Engine を触ってみることにしました。

選択の理由

最初 Windows 上で V8 を使ってみようと思ったのですが( Node.js も V8 を使っているということで)、ビルド自体はできたものの、そこからどうしたら・・・?ということで、いったん中断しました。

で、大好きな FireFox でも使われているという SpiderMonkey を試してみることにしました。

これも最初は Windows 上でビルドしようとしたのですが、途中で登場する autoconf2.13 が、どれを使えば良いのか?となったので、 WSL 上の Ubuntu 18.04 を使うことにしました。

WSL のインストール

あえて書くほどのこともないですが。

コントロール パネル > プログラム > プログラムと機能 > Windows の機能の有効化または無効化

から、 Windows Subsystem for Linux にチェックを入れて再起動します。

せっかくなので WSL 2 もインストールします。 (ビルドバージョン 18917 以上でないと、コマンド実行時にエラーになるので注意が必要です)

あとはディストリビューションMicrosoft Store でインストールするだけです。

今回は Ubuntu 18.04 LTS にしました。

雑にユーザー名とパスワードを登録し、 sudo apt update -> sudo apt upgrade をキメておきます。

Windows <--> WSL(Ubuntu) のファイルのやり取り

Windows と WSL でファイルをやり取りするのは、特別なことをしなくても可能です。

この辺り VM にはない便利さがありますね。

Windows -> WSL(Ubuntu)

Windows 上では下記の場所にデフォルトのディレクトリがあります。

  • C:/Users/{Windows のユーザー名}/AppData/Local/Packages/CanonicalGroupLimited.Ubuntu18.04onWindows_79rhkp1fndgsc/LocalState/rootfs/home/{Ubuntu のユーザー名}

Ubuntu18.04onWindows_ の後ろの部分はインストール時に決まるのでしょうか。

または、アプリのバージョンによっている?

WSL(Ubuntu) -> Windows

WSL 上からは、 /mnt/c が C ドライブになる、と。

ただ、最初 Windows 上に SpiderMonkey のソースを git clone していたため、それを使ってビルドしようとしたら途中で失敗したため、アクセス権的なところで問題があったかと思います。

なので、インストールやビルドするファイルは素直に WSL 上に置くのが良いかと思います。

必要なソフトウェアのインストール

次は SpiderMonkey のビルドに必要なソフトウェアをインストールしていきます。

インストールするものは下記に書かれているわけですが、デフォルトでインストール済みのものと区別がつかない、というところもあって、トライ&エラーで怒られたら足りないものをインストール、という方法で行ったため、不要なもの、重複するものが混じっていたりすると思います。

とにかくインストールしたものは下記の通りです( Rust, Cargo は後述)。

  • autoconf2.13 (普通に autoconf でインストールすると 2.6X とかが入るので注意)
  • python (Python3 系だとちゃんと動かないらしいです)
  • clang
  • gcc
  • g++
  • yasm
  • llvm
  • make
  • clang-8 (多分不要)
  • gcc-6 (多分不要)

Rust, Cargo のインストール

SpiderMonkey のビルドには Rust, Cargo が必要です。

が、 apt get install でそれらを入れてしまうと、バージョンが低いとエラーになります。

ver.1.37.0 が必要なわけですが、これをインストールするのは、またリポジトリー追加するのかな~と思っていたら、少し違っていました。

上記を参考に、コマンドを実行していくだけです。

curl https://sh.rustup.rs -sSf | sh
ls $HOME/.cargo/bin
cat $HOME/.cargo/env
source $HOME/.cargo/env

せっかく環境揃ったのだし、 Rust も触ってみても面白そうですね(収拾付かないので今はできませんが)。

SpiderMonkey のビルド

いよいよここからが本番ですよ。

基本的には MDN のドキュメントを参考に進めていくだけです。

ソースコードは git からも入手可能ということで、下記を参考に git clone しました。

ディレクトリをホームに追加して、その下で git clone 。

cd gecko-dev したところからスタートです。

  1. cd js/src
  2. autoconf2.13
  3. mkdir build_OPT.OBJ
  4. cd build_OPT.OBJ
  5. ../configure
  6. make

3.、4.は不要かもしれません(エラーで苦しんでいるときにあれこれ参考にしたサイトの真似をしています)。

ともかく、エラーが発生することなく make が完了すれば OK です。

それにしても、 make の際、ログが固まりでドン!と出てくるのが心臓に悪い(´・ω・`)

実行する

実行ファイルは gecko-dev/js/src/build_OPT.OBJ/js/src にある js です。

ということで、 (build_OPT.OBJ にいる場合) ./js/src/js と実行してやれば >js と表示され、 console.log("hello world!"); と入力してやれば hello world! と表示されます。

f:id:mslGt:20190915101204j:plain

さて、動くようになったのは良いとして、コンパイルした結果を見る、 JavaScript のコードを直接書くのではなく、ファイルを読み込む方法は? など試していきたいと思います。

【TypeScript】prototype を追いかける

はじめに

続きです。

が、 Decorator から離れていく気しかしないのでタイトルは変えておきます。

prototype メソッド

mainPage.ts

export class Sample{
    private message = "たこやき";
    public callMethod(){
        console.log("I love " + this.message);
        this.message = "チーズ";
    }
    public static callStaticMethod(){
        console.log("I love " + Sample.prototype.message);
        Sample.prototype.message = "コーンポタージュ";
    }
}
~省略~

前回のコードですが、 prototype メソッド( callMethod() )が持つ Object(prototype) をたどってみるため、下記のようにしてみます。

class Sample{
    public callMethod(){
        ~省略~
    }
    
    public getThis(){
        return this;
    }
    
}

let sample = new Sample();

console.log(sample == sample.getThis());

console.log(sample.callMethod);
console.log(sample.callMethod.prototype);
console.log(Object.getPrototypeOf(sample.callMethod));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod))));

まず上記のコードにおいて、 prototype method である callMethod() や getThis() の中で this と書いた場合、その中身は Sample のインスタンスです( sample )。

そのため、sample.callMethod と callMethod() 内での this.callMethod も同じです。

ということで上記は、sample.callMethod のみで確認しています。

実行結果は以下の通り。

true
function callMethod()
undefined
function ()
Object { … }
null

console.log(sample.callMethod) の中身は function callMethod() となっています。

また、その下の Object.getPrototypeOf(sample.callMethod) を繰り返すところを見てみると、下記のようになっています。

function callMethod() > Function.prototype > Object > null

流れとしては関数(メソッド)のプロトタイプチェーンで説明されている通りかと思います。

{インスタンス}.{prototype メソッド}.prototype = undefined?

一つ気になるのが sample.callMethod.prototype 。

なんで undefined になるの?というか、こないだ実行したときそうなってなかったような。。。?と思っていたら、ターゲットを ES2015 以上に設定している場合、返還後の JavaScript でもクラスが使用されるのですが、その場合 undefined になるようです。

ES2015 未満

"use strict";
var Sample = /** @class */ (function () {
    function Sample() {
    }
    Sample.prototype.callMethod = function () {
    };
    Sample.prototype.getThis = function () {
        return this;
    };
    Sample.callStaticMethod = function () {
    };
    return Sample;
}());

ES2015 以上

"use strict";
class Sample {
    callMethod() {
    }
    getThis() {
        return this;
    }
    static callStaticMethod() {
    }
}

なお、 ES3 に設定した場合の sample.callMethod.prototype の結果は下記の通りです。

{…}
    constructor: function callMethod()​
    < prototype>: Object {…}

Object.getPrototypeOf(target) と target.prototype

若干今更感ありますが、 Object.getPrototypeOf(target) と target.prototype の違いとは何でしょうか。

Object.getPrototypeOf(target) (古くは proto ) は、 target の内部プロパティである Prototype を取得するものである、と。

内部プロパティというものが今一つ分かっていないのですが、実際の結果を見ると

console.log(sample.callMethod);
console.log(Object.getPrototypeOf(sample.callMethod));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod))));

この結果が

function callMethod()
function ()
Object { … }
null

メソッド > Punction Prototype > Object > null となっていることから、親の Prototype を取得できる、というイメージでしょうか。

では target.prototype は?というところですが、

この辺りをみると、自身( Constructor 関数)が持つ prototype プロパティであるようです。

…うーん、わかるようなわからないような。。。

この辺りはもう少し調べてみることにします。

prototype メソッドの prototype (図)

先ほどの Object.getPrototypeOf(target) を null が出るまで繰り返した結果を図にしておきます。

f:id:mslGt:20190910223456p:plain

static メソッド

次は static メソッドです。

console.log(Sample.callStaticMethod);
console.log(Object.getPrototypeOf(Sample.callStaticMethod));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Sample.callStaticMethod)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Sample.callStaticMethod))));

結果は。。。

function callStaticMethod()
function ()
Object { … }
null

先ほどとほぼ同じですね。

Sample.callStaticMethod.prototype も同様に undefined になります。

また、 Object.getPrototypeOf(Sample.callStaticMethod)) で得られる Function prototype は prototype method のものと同じです。
(プロパティを追加すると、もう片方でも利用できます。すべきではないと思いますが)

static メソッドの prototype (図)

f:id:mslGt:20190910223525p:plain

関数

では関数だとどうでしょうか。

function callFunction(){   
}
console.log(callFunction);
console.log(Object.getPrototypeOf(callFunction));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(callFunction)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(callFunction))));
console.log(callFunction.prototype);
console.log(Object.getPrototypeOf(callFunction.prototype));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(callFunction.prototype)));

関数の場合は先ほどまでとは違い、 callFunction.prototype で値を得ることができます。

結果は下記の通り。

function callFunction()
function ()
Object { … }
null
{ … }
    constructor: function callFunction()​
    < prototype>: { … }
Object { … }
null

f:id:mslGt:20190910223610p:plain

Object.getPrototypeOf(target) で辿った結果だけを見ると、 prototype メソッドも static メソッドも関数も大きな違いはなさそうです。

関数だけ target.prototype が正しく取れるのはなぜ?と思いはしますが、メソッドの方は単にクラス構文が登場して、値の設定の仕方が変わったから、というだけのような気もします。

この辺も突っ込んで調べてみたいところ。

クラス

今度はクラスです。

JavaScript のクラスは関数の糖衣構文である。。。ということは。。。?

class Sample{
    public callMethod(){
    }
    public getThis(){
        return this;
    }
    public static callStaticMethod(){
    }
}
console.log(Sample);
console.log(Object.getPrototypeOf(Sample));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Sample)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Sample))));
console.log(Sample.prototype);
console.log(Object.getPrototypeOf(Sample.prototype));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Sample.prototype)));
function Sample()
function ()
Object { … }
null
{ … }
    callMethod: function callMethod()​
    constructor: function Sample()​
    getThis: function getThis()​
    < prototype>: { … }
Object { … }
null

ほぼ関数と同じですね。

f:id:mslGt:20190910223708p:plain

クラスインスタンス

クラスインスタンスはどうでしょうか。

let sample = new Sample();

console.log(sample);
console.log(Object.getPrototypeOf(sample));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(sample)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(sample))));

クラスインスタンスは target.prototype を持っていないようです。

結果はこちら。

{}
    < prototype>: Object { … }
{…}​
    callMethod: function callMethod()​
    constructor: function Sample()​
    getThis: function getThis()​
    < prototype>: Object { … }
Object { … }
null

インスタンス自体は Object である、と。

で、 Object.getPrototypeOf(sample) は Sample.prototype と同じ値です。

f:id:mslGt:20190910223738p:plain

マージしてみる

ここまでの図をまとめてみました。

f:id:mslGt:20190910223844p:plain

※オレンジ線は参照の意

こうやって見ると、クラスやクラスインスタンスからはメソッドを参照できていますが、メソッドからはほとんどつながりがなく(大元の Object しかない)、雑に引数としてメソッドを渡すと元のクラスインスタンスを見失ってしまう理由がわかるような気がします(気のせいかも)。

proto が非推奨となったり、動的に親となる Prototype が切り替わる、といった部分を考慮していなかったりと単純化してはいるはずですが、やはり複雑ではありますね。

なお、グローバルオブジェクト( Window )はこんな感じでした。

console.log(this);
console.log(Object.getPrototypeOf(this));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(this)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this))));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this)))));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this))))));
Window 
WindowPrototype { … }
WindowProperties {  }
EventTargetPrototype { addEventListener: addEventListener(), removeEventListener: removeEventListener(), dispatchEvent: dispatchEvent(), … }
Object { … }
null

f:id:mslGt:20190910223903p:plain

うーむ、ちっとも理解できた気がしない。。。

内部的なところに突っ込むともう少しわかったりするのでしょうか。。。

【TypeScript】Decoratorを覗き見る 2

はじめに

今回は Decorator Factory から見てみますよ。

Decorator Factory

正直ドキュメントを見ても Class Decorator を返す関数、というだけでどう活用できるものなのか今一つ分かっていなかったのですが、これを使うことで、 Decorator に引数を渡すことができるようになります。

decoratorFactorySample.ts

import { Sample } from "../sample";

export function decoratorFactorySample(name: string, id: number, s: Sample){
    return function decoratorSample(constructor: Function){
        console.log("generate id:" + id + " name:" + name + " " + constructor);
    }
}

operationSample.ts

import { decoratorFactorySample } from "./decoratorSamples/decoratorFactorySample";

@decoratorFactorySample("john", 1, {call: () => console.log("hello sample"), message: "goodomorning"})
export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    public sayHello(){
        console.log("hello world!");
    }
}

引数の個数や種類に制限は無いようで、上記のように増やしても問題なく実行できました。

逆に引数を 0 にすると。。。

operationSample.ts

import { decoratorFactorySample } from "./decoratorSamples/decoratorFactorySample";

@decoratorFactorySample()
export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    public sayHello(){
        console.log("hello world!");
    }
}

おや?これ InversifyJS を利用するコードで見たことあるような。。。
(まだコードを読んでいないので確証はありませんが)

なお当然とも言えますが、この後登場する Method Decorator など Class 以外の Decorator でも使用できます。

Method Decorator

お次は Method Decorator です。

名前の通りメソッドに付与する Decorator ですが、今度は引数が 3 つあります。

methodDecoratorSample.ts

export function methodDecoratorSample(target: any, propertyKey: string, discripter: PropertyDescriptor){
    console.log("method decorator start");
    console.log(target);
    console.log(propertyKey);
    console.log(discripter);    
    console.log("method decorator end");
}

operationSample.ts

import { methodDecoratorSample } from "./decoratorSamples/methodDecoratorSample";

export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    @methodDecoratorSample
    public sayHello(){
        console.log("hello world!");
    }
}

実行すると下記のようになります。

method decorator start
Object { sayHello: sayHello(), … }
sayHello
Object { value: sayHello(), writable: true, enumerable: true, configurable: true }
method decorator end

第一引数はメソッドを定義しているクラス、第二引数は対象のメソッド名、第三引数はメソッドの情報、ということのようです。

出力結果を見ると、 PropertyDescriptor.value は対象のメソッドであるため、 PropertyDescriptor.value.call(target) のようにすれば呼び出すことができます。

が、ドキュメントによると target を ES5 より低く設定している場合は PropertyDescriptor が undefined になるようなので、 ES3 に設定している、またはその恐れがある場合は使わない方が良いかもしれません。

なお Mastering TypeScript 3 では第三引数を optional にしていました(現行バージョンとの違いによるものかもしれませんが)。

なお、 PropertyDescriptor.value を使う以外に関数を実行する方法として、下記があります。

target[propertyKey]();

関数の呼び方

ここまで同じ関数(メソッド)を呼ぶのにいくつかの方法が登場しました。

例えばこんなクラスがあったとして

export class Sample{
    private message = "たこやき";
    public callMethod(){
        console.log("I love " + this.message);
        this.message = "チーズ";
    }
    public static callStaticMethod(){
        console.log("I love " + Sample.prototype.message);
        Sample.prototype.message = "コーンポタージュ";
    }
}

これらのメソッドを呼ぶ方法として下記が挙げられます
( 6 、 7 、 8 は 2 、 3 、 4 と同じですが、static メソッドということで追加しました)。

export function doSomething(){
    let sample = new Sample();
    // 1
    sample.callMethod();
    // 2
    sample.callMethod.call(sample);
    // 3
    sample.callMethod.apply(sample);
    // 4
    sample["callMethod"]();
    // -- static method --
    // 5
    Sample.callStaticMethod();
    // 6
    Sample.callStaticMethod.call(Sample);
    // 7
    Sample.callStaticMethod.apply(Sample);
    // 8
    Sample["callStaticMethod"]();
}

1 、5 は C# でもおなじみの方法ですが、他はあまり見慣れない呼び方です。

実行結果は下記の通りです(どれがどの結果かわかるように番号を振っています)。

// 1
I love たこやき
// 2
I love チーズ
// 3
I love チーズ
// 4
I love チーズ
// 5
I love undefined
// 6
I love コーンポタージュ
// 7
I love コーンポタージュ
// 8
I love コーンポタージュ

1 ~ 4 までのインスタンスを作って呼んでいるメソッド( prototype メソッド)は、呼び方はともかく想定通りの動きではあります。

5 は2回繰り返すと、他の static メソッド同様「I love コーンポタージュ」となります。

ということは、プロパティ message の定義時に渡している「たこやき」を受け取ることはできず、 かつ static メソッド間では状態が保持されていることがわかります。

なお最後に 1 を追加するともう一度「I love チーズ」と出力されるため、 prototype メソッド側(というか生成したインスタンス)も static メソッドの状態を見ていないことがわかります。

早速それぞれのメソッドを見ていきたいのですが( prototype も気になる)、その前に(見た目上)なじみのある書き方である 1 、5 について。

実は、 JavaScript(TypeScript) におけるクラス、というのは、関数の糖衣構文であるとのこと。

そのため先ほどのクラスは、 ES3 で変換すると下記のようになります( TypeScript Playground で確認) 。

"use strict"
var Sample = /** @class */ (function () {
    function Sample() {
        this.message = "たこやき";
    }
    Sample.prototype.callMethod = function () {
        console.log("I love " + this.message);
        this.message = "チーズ";
    };
    Sample.callStaticMethod = function () {
        console.log("I love " + Sample.prototype.message);
        Sample.prototype.message = "コーンポタージュ";
    };
    return Sample;
}());

インスタンスを作って呼んでいる callMethod はその名の通り prototype を通して、 static メソッドである callStaticMethod はそのまま Sample につながっています。

いつも通り迷走感がすごいですが、今回は prototype を見てみることにしましょう。

Prototype

上記を見ると、まず JavaScript では、関数(メソッド)・プロパティなどの種類にかかわらず、そのほとんどが prototype という Object を継承していると。

先ほどの実験から prototype メソッドと static メソッドで見ているものが違う、ということはわかりました。

ではそれぞれどう違うのか、とりあえずコンソールに出力して比べてみます。

まずは prototype メソッドから。

mainPage.ts

export function doSomething(){

    let sample = new Sample();

    console.log(sample.callMethod);
    console.log(sample.callMethod.prototype);
}

結果はこちら。

// console.log(sample.callMethod);
callMethod()​
    length: 0
    name: ""
    prototype: {…}
        constructor: function callMethod()​​
        < prototype>: Object { … }
    < prototype>: ()
        apply: function apply()
        arguments: 
        bind: function bind()
        call: function call()
        caller: 
        constructor: function Function()
        length: 0
        name: ""
        toSource: function toSource()
        toString: function toString()
        Symbol(Symbol.hasInstance): function Symbol.hasInstance()
        < get arguments()>: function arguments()
        < set arguments()>: function arguments()
        < get caller()>: function caller()
        < set caller()>: function caller()
        < prototype>: Object { … }

// console.log(sample.callMethod.prototype);
{…}
    constructor: function callMethod()​
    < prototype>: {…}
        __defineGetter__: function __defineGetter__()
        __defineSetter__: function __defineSetter__()
        __lookupGetter__: function __lookupGetter__()
        __lookupSetter__: function __lookupSetter__()
        __proto__: 
        constructor: function Object()
        hasOwnProperty: function hasOwnProperty()
        isPrototypeOf: function isPrototypeOf()
        propertyIsEnumerable: function propertyIsEnumerable()
        toLocaleString: function toLocaleString()
        toSource: function toSource()
        toString: function toString()
        valueOf: function valueOf()
        < get __proto__()>: function __proto__()
        < set __proto__()>: function __proto__()

では static メソッド。

mainPage.ts

export function doSomething(){
    console.log(Sample.callStaticMethod);
    console.log(Sample.callStaticMethod.prototype);
}

結果です。

// console.log(Sample.callStaticMethod);
callStaticMethod()​
    length: 0
    name: ""
    prototype: {…}
        constructor: function callStaticMethod()​​
        < prototype>: Object { … }
    < prototype>: ()
        apply: function apply()
        arguments: 
        bind: function bind()
        call: function call()
        caller: 
        constructor: function Function()
        length: 0
        name: ""
        toSource: function toSource()
        toString: function toString()
        Symbol(Symbol.hasInstance): function Symbol.hasInstance()
        < get arguments()>: function arguments()
        < set arguments()>: function arguments()
        < get caller()>: function caller()
        < set caller()>: function caller()
        < prototype>: Object { … }

// console.log(Sample.callStaticMethod.prototype);
{…}​
    constructor: function callStaticMethod()​
    < prototype>: {…}
        __defineGetter__: function __defineGetter__()
        __defineSetter__: function __defineSetter__()
        __lookupGetter__: function __lookupGetter__()
        __lookupSetter__: function __lookupSetter__()
        __proto__: 
        constructor: function Object()
        hasOwnProperty: function hasOwnProperty()
        isPrototypeOf: function isPrototypeOf()
        propertyIsEnumerable: function propertyIsEnumerable()
        toLocaleString: function toLocaleString()
        toSource: function toSource()
        toString: function toString()
        valueOf: function valueOf()
        < get __proto__()>: function __proto__()
        < set __proto__()>: function __proto__()

うーん。。。 メソッド名の違いを除くとあまり違っているようには見えませんね。。。

見てるところが違っているのかしら。。。

一旦切ります。

【TypeScript】Decoratorを覗き見る 1

はじめに

ここまで InversifyJS のコードを追いかけてみたわけですが、どうもどのようにしてインスタンスを生成してきているのかがわからない。。

もちろん詳しいところは get の部分を追う必要はあるのですが、一旦その前に関連していそうな Decorator を調べてみることにしました。

Decorator について

雑に調べたところ、下記のような特徴があるようです。

  • 定義されたクラスに要素(プロパティや関数など)を動的に追加できる。
  • その中身は関数である。
  • @関数名 のように Java におけるアノテーションのようにクラスや関数、プロパティに付与して使用する。
  • Decorator の関数の引数は付与対象(クラス、関数、プロパティ)によって異なる。
  • 実行されるのは Decorator が付与されたクラスが定義された時であり、インスタンス化された時ではない。
  • まだ Experimental な機能であり、デフォルトでは有効になっていない。

準備

前述の通り、デフォルトでは有効になっていないため、 tsconfig.json を変更します。

Decorator には直接関係ないところも混ざっていますが、下記のように設定しました。

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["dom", "es6"],
    ~省略~
    "outDir": "dist/js",
    ~省略~
    "strict": true,
    ~省略~
    /* Experimental Options */

    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    
  }
}

で、 HTML からの呼び出し用の関数と Decorator 付与用のクラスを用意します。

import/export はいつも通り webpack を使用します。

mainPage.ts

import { OperationSample } from "./operationSample";

export function doSomething(){
    console.log("start");

    let o = new OperationSample();
    o.sayHello();
    
    console.log("end");
}

operationSample.ts

export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    public sayHello(){
        console.log("hello world!");
    }
}

MainPage.html

< !DOCTYPE html>
< html lang="ja">
    < head>
        < meta charset="utf-8">
        < title>Decorator Sample< /title>
    < /head>
    < body>
        < button onclick="Page.doSomething()">hello< /button>
        < script src="../js/main.bundle.js">< /script>
    < /body>
< /html>

これでボタンを押すと下記が出力されます。

start
constructor death
hello world!
end

Class decorator

まず Class に Decorator を付与してみます。

classDecoratorSample.ts

export function classDecoratorSample(constructor: Function){
    console.log("classDecoratorSample");
    
    // OperationSample の constructor が実行される.
    // 戻り値は undefined.
    let c = constructor();
}

operationSample.ts

import { classDecoratorSample } from './classDecoratorSample';

@classDecoratorSample
export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    public sayHello(){
        console.log("hello world!");
    }
}

Class decorator として渡される関数の引数は、Decorator が付与されたクラス(今回は OperationSample )の constructor です。

そのため、上記のコードを実行するとページを読み込んだ時点で OperationSample の constructor が実行され、「constructor death」が出力されます。

なおコメントとして記載しましたが、 C# などとは違い constructor の戻り値はそのクラスのインスタンスではありません。
(undefined となる)

じゃあ初期化できるだけ?かというとそうではありません。

( constructor に限りませんが) TypeScript(JavaScript) の関数は prototype という object を継承しています。

この prototype 、デフォルトでは any 型ですが、アサーションを使って元のクラスの型( OperationSample )に変換することができます。

classDecoratorSample.ts

export function classDecoratorSample(constructor: Function){
~省略~

    // OperationSample として扱うことができる.
    let c = constructor.prototype as OperationSample;

    // hello world! と出力される.
    c.sayHello();
}

ちなみにこのアサーション、「as OperationSample」のクラス名が「as "OperationSample"」と文字列であっても、正しく OperationSample 型に変換してくれたりします。

classDecoratorSample.ts

export function classDecoratorSample(constructor: Function){
~省略~

    // 先ほど同様 OperationSample に変換される.
    let c = constructor.prototype as "OperationSample";

    // hello world! と出力される.
    c.sayHello();
}

ただし TypeScript 上の型は "OperationSample" であるため、 c.sayHello() の部分はコンパイルエラーになりますが。。。

前回までのコードを見返したり、 get を見ないとわかりませんが、 InversifyJS で依存クラスのインスタンスを取ってくるあたりに関係していそうな気がします。

元クラスで定義されていない関数やプロパティを追加する

またこの prototype に OperationSample で定義されていないプロパティや関数を渡すことで、 OperationSample にプロパティや関数を追加できます。

classDecoratorSample.ts

export function classDecoratorSample(constructor: Function){
    console.log("classDecoratorSample");
    
    constructor.prototype.greeting = () => console.log("こんにちは");
}

あとはこのように呼んでやれば OK です。

mainPage.ts

export function doSomething(){
~省略~

    o.greeting();
    
~省略~
}

が、当然ながら定義が見つからないとコンパイルエラーになります。

定義ファイルを用意することで、これを防ぐことができます。

下記を参考に src/types というディレクトリ以下に operationSample.d.ts というファイルを作成したところ、コンパイルエラーが解消されました。

operationSample.d.ts

import {OperationSample} from "../ts/operationSample"

declare module "../ts/operationSample"{
    export interface OperationSample{
        greeting(): any;
    }
}

定義の中身

さて気になるのはその中身。

「declare module ~」の部分は、 Ambient Modules と呼ばれる機能で、既存の JavaScript ライブラリなどを使う際に定義を追加・設定するために使われるものであるようです。

まぁコードの見た目的にも、 operationSample.ts というモジュールに対して OperationSample という interface を設定している、という内容に読めます。

んで、その OperationSample という interface についてです。

この interface の名前はクラス名と同じなわけですが、 C# 脳だとどうしても「なぜ同じ名前?エラーにならないの?」と思ってしまいます。

両者の名前が同じである必要があるのは、 TypeScript の interface が open-ended であるため(だと思います)です。

open-ended というのは、このような コードがあったとして、

interface ISample{
    greeting(): void;
}
interface ISample{
    doNothing(): void;
}

最終的に下記の interface と同じ結果になる、ということです。

interface ISample{
    greeting(): void;
    doNothing(): void;
}

で、最初これを interface 間だけの話だと思っていたのですが、これにクラスを加えてみると。。。

class ISample{

}
interface ISample{
    greeting(): void;
}
interface ISample{
    doNothing(): void;
}

ISample というクラスが 2 つの interface を継承した状態となります。

function doSomething(){
    let s = new ISample();
    s.greeting();
    s.doNothing();
}

まぁ実装していないので動かないのですが。。。

ということで、用意した定義ファイルによってクラス OperationSample は interface の OperationSample を継承した状態になるため、関数 greeting() が呼び出し元からも見られるようになった、というわけですね。

なるほどなるほど。面白いですねぇ :)

ということで長くなってきたので一旦切ります。

【TypeScript】InversifyJSでIoC(DI)体験 2

はじめに

*【TypeScript】InversifyJSでIoC(DI)体験 1

続きです。

今回は Container クラスを中心に、もう少し InversifyJS の中身を見ていきたいと思います。

ContainerOptions

前回は使用しませんでしたが、 Container クラスの Constructor では引数としてオプションを指定できます。

内容は Wiki の通りなのですが、せっかくなので試してみますよ。

interfaces/interfaces.ts

~省略~
export interface ContainerOptions {
    autoBindInjectable?: boolean;
    defaultScope?: BindingScope;
    skipBaseClassChecks?: boolean;
}
~省略~

autoBindInjectable

autoBindInjectable を true にすると、下記のようにインスタンスを取得できるようになります。

sampleContainer.ts

const sampleContainer = new Container({ 
    autoBindInjectable: true
});
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();

mainPage.ts

export function doSomething(){
  // sampleContainer.get< Sample>(TYPES.Sample) のように指定する必要がない.
  let s = sampleContainer.get(DecoratorSample);
}

一瞬良いかも、と思ってみたのですが、これだと呼び出す側も実装クラスが誰なのかを知っている必要があるため、どうなんだろう……と思ってしまいました。

もちろん私が気づいていないだけで良い使い方があるかもしれませんが。

defaultScope

defaultScope を指定すると、前回登場した inSingletonScope() などのスコープ設定をデフォルトで設定できます。

sampleContainer.ts

// Default Scope無し.
const sampleContainer1 = new Container();
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();

// Default Scopeあり.
const sampleContainer2 = new Container({
    defaultScope:"Singleton"
});
sampleContainer2.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample);

sampleContainer1 、 sampleContainer2 ともにシングルトンで DecoratorSample のインスタンスが設定されます。

skipBaseClassChecks

一番よく分かっていない機能ですが、拡張クラスを Inject したい場合に便利らしいです。

interface について

ちょっとわき道にそれますが。

Container クラスの Constructor の引数は ContainerOption です。

が、実際には下記のように引数を渡しています。

const sampleContainer = new Container({
    defaultScope:"Singleton"
});

あれ? ContainerOption (の具象クラス) ではない?

また、先ほど見逃していましたが、 TypeScript の interface は C# とは異なり、変数(プロパティ)を持たせることもできるようです。

interfaces/interfaces.ts

~省略~
export interface ContainerOptions {
    autoBindInjectable?: boolean;
    defaultScope?: BindingScope;
    skipBaseClassChecks?: boolean;
}
~省略~

また、具象クラスを定義してインスタンスを作らなくても、 interface が持つプロパティや関数を実装していれば同じものとして扱うことができます。
(この辺りは Go などと(今も仕様が変わっていなければ)共通していますね)

例えばこういうクラスと interface があったとして。

export interface Sample{
    message: string;
    call(): any;
}
export class IntefaceSample{
    public constructor(sample: Sample){
        console.log(sample.message);
    }
}

IntefaceSample のコンストラクタに下記を渡すことができます。

let sample = {
    message: "hello",
    call: () => {}
};

let s = new IntefaceSample(sample);

先ほどの Container のコンストラクタではこの仕組みを利用していたわけですね。

あまり調子に乗って使いすぎるとわけわからん状態になりそうではありますが、こういう柔軟さは良いですね。

Container を追う

さて、 InversifyJS に戻りますよ。

まずは DI Container である(はずの) Container クラスのコンストラクタからたどっていきます。

コンストラクタの最初の処理は先ほどの ContainerOption の設定(引数で渡されていれば)を行っています。

後々 Option の指定によってどのような処理が行われるか、ということにも触れることにはなると思いますが、ここではスキップします。

で、残りの処理はこちら。

container/container.ts

~省略~
public constructor(containerOptions?: interfaces.ContainerOptions) {
~省略~
    this.id = id();
    this._bindingDictionary = new Lookup< interfaces.Binding< any>>();
    this._snapshots = [];
    this._middleware = null;
    this.parent = null;
    this._metadataReader = new MetadataReader();
}
~省略~

id

処理は utils/id.ts にあります。

やっていることは 0 からカウントアップした値を返す、というだけのシンプルなものです。

C# で Equals をオーバーライドする時に出てきた HashCode のようなものでしょうか。

_bindingDictionary

Lookup 、 Binding といういかにも重要そうなクラス、 interface が登場しました。

Lookup は container/lookup.ts 、 Binding は interfaces/interfaces.ts で定義されています。

_metadataReader

planning/metadata_reader.ts で定義されているクラスで、 metadata という名前や Reflect.getMetadata() を呼んでいたりと、 inject するクラスの取得などに関連していると思われます。
( @injectable() を付与するクラスで import が必要だった metadata_reader がコレですね)

空にしているだけであったためスキップした snapshots 、 middleware 、 parent も気になるところですが、さくさく次に進みましょう。

Container.bind

Inject する interface と具象クラスからインスタンスを取得する bind を見てみます。

container/container.ts

~省略~
    // Registers a type binding
    public bind< T>(serviceIdentifier: interfaces.ServiceIdentifier< T>): interfaces.BindingToSyntax< T> {
        const scope = this.options.defaultScope || BindingScopeEnum.Transient;
        const binding = new Binding< T>(serviceIdentifier, scope);
        this._bindingDictionary.add(serviceIdentifier, binding);
        return new BindingToSyntax< T>(binding);
    }
~省略~

まず 「const scope = this.options.defaultScope || BindingScopeEnum.Transient;」であれ?と思いましたが、これは this.options.defaultScope が undefined であった場合は BindingScopeEnum.Transient が渡されます。

ということで、何も指定しなかった場合のデフォルトのスコープは Transient になるのですね。

と思っていたら(当然ながら)ドキュメントで触れられていました。

serviceIdentifier

引数の serviceIdentifier ですが、前回使用した string 、 symbol の他 Newable つまりクラス、抽象クラスも扱うことができるようです。

interfaces/interfaces.ts

~省略~
export type ServiceIdentifier< T> = (string | symbol | Newable< T> | Abstract< T>);
~省略~

binding

bindings/binding.ts で定義されているクラスですが、ここでは先ほどの serviceIdentifier とスコープ情報を保持しています。

ここで作られた Binding< Sample>("Sample", BindingScopeEnum.Singleton) が、 container/lookup.ts の Map に格納されます。

container/lookup.ts

~省略~
private _map: Map< interfaces.ServiceIdentifier< any>, T[]>;
~省略~

この値は途中で出てきた _bindingDictionary に格納され、 Container クラスからアクセスされると。

で、この関数の中で最後に返されるのは syntax/binding_to_syntax.ts の BindingToSyntax< T> です。

BindingToSyntax< T>

BindingToSyntax クラスでは Binding< Sample>("Sample", BindingScopeEnum.Singleton) を持っています。
(コンストラクタで渡される)

んで、はじめに戻って今回作成した sampleContainer を見てみると、 to() を呼んでいます。

sampleContainer.ts

sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample);

syntax/binding_to_syntax.ts

~省略~
public to(constructor: new (...args: any[]) => T): interfaces.BindingInWhenOnSyntax< T> {
    this._binding.type = BindingTypeEnum.Instance;
    this._binding.implementationType = constructor;
    return new BindingInWhenOnSyntax< T>(this._binding);
}
~省略~

この引数で DecoratorSample (interface Sample の具象クラス)の情報が _binding に追加されます。

BindingInWhenOnSyntax

syntax/binding_in_when_on_syntax.ts で定義されているクラスです。

in when on 。。。 いえ、良いんですよ。

syntax/binding_in_when_on_syntax.ts

~省略~
    public constructor(binding: interfaces.Binding< T>) {
        this._binding = binding;
        this._bindingWhenSyntax = new BindingWhenSyntax< T>(this._binding);
        this._bindingOnSyntax = new BindingOnSyntax< T>(this._binding);
        this._bindingInSyntax = new BindingInSyntax< T>(binding);
    }
~省略~

要は in when on でそれぞれインスタンスを作っていると。

Container インスタンスを保持している sampleContainer.ts で以前のように inSingletonScope() を実行した場合、 this._bindingInSyntax が使用されるようですね。

ここまでで、 interface 、具象クラス、スコープ情報が保持されているところが何となく見られてような気がします。

スコープの種類にかかわらず、実際にインスタンスが作られるのは最初に Container にインスタンスを取りに行ったタイミング。

ということで、 get を追うといよいよインスタンスが生成されたり保持されたりするところが見られる、ということ。。。だと思います。多分。

【TypeScript】InversifyJSでIoC(DI)体験 1

はじめに

諸般の事情から TypeScript に漬け込まれている今日この頃。

ふと TypeScript 単体で DI ってできないのかな~と思ってググった時に出てきた、 InversifyJS を試してみることにしました。

TypeScript 単体で~というのは、(今回の InversifyJS でも使われている) Decorator を使うとできそうなので、こちらも引き続き追いかけてはいきたいと思います。

なおタイトルでわざわざ IoC(Inversion of control) を出したのは、 InversifyJS の名前がそこから来てるんだろうな~と思ったためで大した理由はありません。

準備

何はともあれインストールです。

GitHub の手順に従って進めますよ。

npm install --save-dev inversify reflect-metadata

tsconfig.json

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": ["dom", "es6"],                              /* Specify library files to be included in the compilation. */

    ~省略~

    "outDir": "wwwroot/js",                        /* Redirect output structure to the directory. */

~省略~

    "strict": true,                           /* Enable all strict type-checking options. */
  
~省略~

    "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */

~省略~

    "esModuleInterop": true,                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

~省略~

    "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}

コメントアウトされた部分は省略しています。また変更したところは太字にしています。

あと、ベースにしているプロジェクトは以前使ったものをそのまま再利用している関係もあり、 webpack を使っています。

exports が使えない?

以前は HTML から呼び出す関数を下記のように書いていました。

exports.doSomething = function(){
    console.log("hellllloooo");
}

が、 tsconfig.json で「"types": ["reflect-metadata"],」を有効にしようとするとエラーになってしまいました。

どういう関係があるのはよくわかっていないのですが。。。

で、下記のように変更しました。

export function doSomething(){
    console.log("hellllloooo");
}

呼び出す側は変わらず Page.doSomething() で OK です。

……いや、これできるならそもそもこれでよかったんやけど感。

ま、まぁいいや。

とりあえず DI ってみる

準備もできたところで早速使ってみましょう。

sample.ts

export interface Sample{
    call(): any;
}

DI で挿入する interface です。

特に変わったところはないです。

decoratorSample.ts

import { Sample } from "./sample";
import 'reflect-metadata';
import {injectable} from 'inversify';

@injectable()
export class DecoratorSample implements Sample{
    public call(){
        console.log("hello2");
    }
}

Sample の実装クラスです。

@injectable() なる気になるワードが。

types.ts

const TYPES = {
    Sample: Symbol.for("Sample")
}
export { TYPES }

次に登場する Container で使用します。

sampleContainer.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { Sample } from "./sample";
import { DecoratorSample } from "./decoratorSample";

const sampleContainer = new Container();
sampleContainer.bind< Sample>(TYPES.Sample).to(DecoratorSample);

export{ sampleContainer }

ここで Inject される interface と実装クラスの紐づけが行われています。

では Inject されたものを利用してみます。

mainPage.ts

import { Sample } from "./sample";
import { sampleContainer } from "./sampleContainer";
import { TYPES } from "./types";

export function doSomething(){
    let s = sampleContainer.get< Sample>(TYPES.Sample);
    s.call();
}

ASP.NET Core のコンストラクタインジェクションなどより長くなってしまっているのは気になりますが、これでインスタンスを取得できます。

ただしシングルトンではない

このままだとシングルトンではないため、上記とは別に sampleContainer.get でインスタンスを取得すると別インスタンスが渡されてしまいます。

この指定は Container で行います。

sampleContainer.ts

~省略~
const sampleContainer = new Container();
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();
~省略~

reflect-metadata 大事

注意すべき点は Inject 対象のクラスで reflect-metadata を import することです。

これを忘れると下記のエラーが発生します。

TypeError: Reflect.hasOwnMetadata is not a function

コンパイルエラーは発生せず、見落としがちなので注意が必要です。

InversifyJS とたわむれる 1

というわけで無事 DI できました。

もう少し中身を見てみることにします。

インスタンス生成のタイミング

Inject 対象のクラスインスタンスですが、いつ生成されるのでしょうか。

decoratorSample.ts

~省略~
@injectable()
export class DecoratorSample implements Sample{
    
    public constructor(){
        console.log("Constructor death");
    }
    
    public call(){
        console.log("hello2");
    }
}

mainPage.ts

~省略~
export function doSomething(){
    
    console.log("start");
    
    let s = sampleContainer.get< Sample>(TYPES.Sample);
    s.call();
}

これを実行すると、出力結果は下記のようになります。

start
Constructor death
hello2

では Inject 対象のクラスが二つ以上ある場合はどうでしょうか。

doSomething.ts

export interface DoSomething{
    saySomething():void;
}

diDoSomething.ts

import 'reflect-metadata';
import {injectable} from 'inversify';
import { DoSomething } from './doSomething'

@injectable()
export class DiDoSomething implements DoSomething{
    public constructor(){
        console.log("DoSomething constructor death");
    }
    public saySomething():void{
        console.log("ho");
    }
}

types.ts

const TYPES = {
    Sample: Symbol.for("Sample"),
    
    DoSomething: Symbol.for("DoSomething")
    
}
export { TYPES }

sampleContainer.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { Sample } from "./sample";
import { DecoratorSample } from "./decoratorSample";
import { DoSomething } from "./decoratorSamples/doSomething";
import { DiDoSomething } from "./decoratorSamples/DiDoSomething";

const sampleContainer = new Container();
sampleContainer.bind< Sample>(TYPES.Sample)
    .to(DecoratorSample)
    .inSingletonScope();
    
sampleContainer.bind< DoSomething>(TYPES.DoSomething)
    .to(DiDoSomething)
    .inSingletonScope();
    
export{ sampleContainer}

mainPage.ts

~省略~
export function doSomething(){
    
    console.log("start");
    
    let s = sampleContainer.get< Sample>(TYPES.Sample);
    s.call();
}

これを実行すると、出力結果は下記のようになります。

start
Constructor death
hello2

ということで、下記のことがわかりました。

  1. ( inSingletonScope の場合)最初に sampleContainer.get が実行されたタイミングで実体化される。
  2. sampleContainer.get が実行されていないクラスは同じ Container にバインドされていても実体化されない。

TYPES

Container への bind や get で使用されている TYPES 。

types.ts

const TYPES = {
    Sample: Symbol.for("Sample"),
    DoSomething: Symbol.for("DoSomething")
}
export { TYPES }

今回の用途である sampleContainer.get< Sample> の引数の型は string であり、その内容は Inject される interface 名となります。
(実は実装クラス名でも問題なく動作はしますが、あえてそうするメリットはなさそうです)

sampleContainer.get< Sample>("Sample") のように直接 string で指定することはできますが、引数の名前が実際のクラス名( interface 名)と違っていると実行時にエラーが発生するため、一か所にまとめるという発想は理解ができます。

が、それを const Sample: string = "Sample"; のようにせずに Symbol を使っているのはなぜでしょうか。

Symbol

そもそも Symbol とは?という話になるわけですが、上記を見たところ以下のような特徴があるようです。

  1. ECMAScript 6 から登場したプリミティブ型
  2. ユニークかつ不変である
  3. string と同じくプロパティのキーとして使用できる

特に 2.の特徴から ID として使われることから、今回も string ではなく Symbol が使われているようです。

なお、 InversifyJS の README によると、 Symbol 、 string での指定の他、 class でも指定できるようです。

ただし Symbol 推奨である、ということでした。

なお、 Symbol について注意が必要なのは、下記かなと思っています。

console.log(Symbol.for("Sample") === Symbol.for("Sample"));

console.log(Symbol("Sample") === Symbol("Sample"));

結果は下記になります。

true
false

Symbol("Sample") はユニークな値を作るのに対し、 Symbol.for("Sample") はグローバルシンボルテーブルと呼ばれる場所に値を作成し、ページをまたいだりしても Symbol.for の引数が同じであれば同じ値を返します。

そのため上記はこのようにしても true となります。

console.log(Symbol.for("Sample") == Symbol.for("Sample"));

InversifyJS については引き続き中身を追っていきたいと思います。

Symbol についても使い方が今一つよくわかってはいないため、ゆるゆると自分でも使用していきたいと思います。