vaguely

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

【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() が呼び出し元からも見られるようになった、というわけですね。

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

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