TypeScriptのデコレータってちょっとPythonと比較してイマイチじゃないですか？

## 読み飛ばしてください

どうも限界派遣SESです。
本日もひとりアドカレ10日目やっていきます。

最近毎日記事を書くようになってネタ探しのために色々と調べたりコードを書いたりするようになりました。
やっぱり、自分で期日を決めて作業をするのは良いですね。睡眠時間は削れ気味ですが。

## 調べるにあたった経緯

まず最初にTypeScriptのデコレータについて調べようとした経緯です。

最近ReactとTailwindcssでVSCodeの拡張機能を作る記事を書いていたのですが、その中でWebViewとVSCodeの間で通信をする必要があり少々冗長気味になってしまうという問題がありました。

```ts
// WebView側の処理
vscode.postMessage({
    command: 'hello',
    name: 'やまだじろう'
});


// VSCode側の処理
panel.webview.onDidReceiveMessage(
    message => {
        switch (message.command) {
            // helloというコマンドが呼ばれた時の処理をよしよしする
            case 'hello':
                const name = message.name as string;
                
                vscode.window.showInformationMessage(`${name}さん、こんにちは`);
        }
    },
    undefined,
    context.subscriptions
);
```

実際には別々のファイルで管理するのでこれらは相互でどんな型が送られてくるかわからないという問題などあります。
型は`types/hello.ts`などのファイルを定義してお互いで利用すればある程度担保できると思いますが、呼び出すための`hello`という文字列で呼び出し先を指定しなければいけません。

なので、Pythonで作っていたDiscordのBotのようなライブラリの構文で書けるのが理想的なのでは？と思い調べてみることにしました。

### Pythonでのデコレータ

Pycordを利用したDiscordBot開発だとデコレーターを使って簡単にコマンドを定義できます。

公式のExamplesから抜粋した以下のコードのような書き方でこれがコマンドであることや、コマンド引数に`name`, `gender`, `age`があり、関数名から`hello`コマンドであることがわかります。

https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_options.py


```py
import discord
from discord import option

bot = discord.Bot(debug_guilds=[...])

# hello コマンドの定義
@bot.slash_command()
@option("name", description="Enter your name")
@option("gender", description="Choose your gender", choices=["Male", "Female", "Other"])
@option(
    "age",
    description="Enter your age",
    min_value=1,
    max_value=99,
    default=18,
)
async def hello(
    ctx: discord.ApplicationContext,
    name: str,
    gender: str,
    age: int,
):
    await ctx.respond(
        f"Hello {name}! Your gender is {gender} and you are {age} years old."
    )

bot.run("TOKEN")
```

とても、わかりやすいですよね。
これと同じようにTypeScriptデコレータでコマンド登録が可能であればキレイに書けそうですよね。


## TypeScriptでのデコレータ

まず、TypeScriptのドキュメントのDecoratorsの項目と構文をそのまま利用しようとすると引数が違うなどと怒られます。
ドキュメントで登場しているデコレータの構文はStage2で5.0はStage3となっていて仕様が変わっているそうです。（英語読めないので読み飛ばしていましたが一番上に書いてます。）

https://www.typescriptlang.org/docs/handbook/decorators.html#property-decorators


TypeScriptのデコレータでサポートしているのは

- クラス宣言
- メソッド
- アクセサ
- プロパティ
- パラメーター

の5種類です。

つまり、クラスの中でしか使えません。

### 関数に対してのデコレータが利用できない

Pythonで定義する場合は以下のようなイメージで、関数に対してデコレータをつけることが出来ます。簡潔で良いですね。

```py
from functools import wraps

def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f'{func.__name__} called')
        return func(*args, **kwargs)
    return wrapper

@logging
def hello():
    print('Hello, World!')
```

もし、TypeScriptで関数的としてデコレータ扱うのであれば、シングルトンパターンで実装するのが現実的でしょう。

```ts
// 前後でログを出力するデコレータ
function logging(target: Function, context: DecoratorContext) {
    const name = context.name?.toString();
    return function (this: any, ...args: any[]) {
        console.log(`starting ${name} with arguments ${args.join(", ")}`);
        const ret = target.call(this, ...args);
        console.log(`ending ${name}`);
        return ret;
    };
}

class Hello {

    // メソッドにデコレータを適用
    @logging
    sayHello() {
        console.log("はろー");
    }
}

// デコレータを適用したクラスのインスタンスをエクスポート
export default new Hello();
```

これで実行すると以下のように前後でログを出力できます。

```
starting sayHello with arguments 
はろー
ending sayHello
```


### クラスに対して新しいメソッドをした際の問題

クラス宣言の際にデコレータをつける場合は以下のようなイメージでつけられます。
以下の例では`Hello`クラスに対して`AddMethod`をつけることで`Hello`クラスを継承した無名クラスを返却します。
この時、無名クラスに新規のメソッドを付与するなどが出来ます。

```ts
function AddMedhod<T extends { new(...args: any[]): {} }>(target: T, context: DecoratorContext) {
    return class extends target {
        // 継承クラスに対してメソッドを付与
        newMethod() {
            console.log("new method");
        }
    };
}

@AddMedhod
class Hello {
}
```

しかし実際にこのメソッドを利用する際は`newMethod`の存在を認知してくれません。
そのため、この方法で継承した無名クラスは`@ts-ignore`などを使って無理やり呼び出して上げる必要があります。
TypeScriptなのに型がわからないのは辛いよなぁ。と思いながら見てました。

```ts
const hello = new Hello();
// @ts-ignore
hello.newMethod(); // このメソッドの存在を認知してくれない
```

ちなみにPythonだと以下で`Pylance`による型推論でメソッドが増えた事も認識してくれます。

```py
# メソッドを追加するデコレータ
def add_method(_cls):
    class Wrapper(_cls):
        def new_method(self):
            print(f'{self.__class__.__name__}.new_method called')
    return Wrapper

# デコレータをクラスを定義
@add_method
class Hello:
    def say_hello(self):
        print('Hello, World!')


hello = Hello()

hello.say_hello()
hello.new_method()
```

### デコレータが付与された場合の返却型などを追ってくれない

先ほどの`logging`デコレータの返却値を変えてみます。

```diff_tsx
// 前後でログを出力するデコレータ
function logging(target: Function, context: DecoratorContext) {
    const name = context.name?.toString();
    return function (this: any, ...args: any[]) {
        console.log(`starting ${name} with arguments ${args.join(", ")}`);
        const ret = target.call(this, ...args);
        console.log(`ending ${name}`);
-        return ret;
+        return 111; // 唐突に111を返してみる
    };
}
```

これを先程のメソッドに付与してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/9c291e04-870b-dc73-58bf-68379faedc18.png)

返却値は`void`型と予想されてしまっています。

実際に以下のコードを実行した場合は`111`が返却されているので、型の予想が間違っています。

```ts
const num = Hello.sayHello();
console.log(num); // 111
```

ちなみにPythonは以下のようなコードで関数がなかったことになります。

```py
# 関数をいっぱい保持するクラス
class FunctionList:
    def __init__(self):
        self.func_list = []

    def add(self, func):
        self.func_list.append(func)

    def run_all(self):
        for func in self.func_list:
            func()


function_list = FunctionList()

# 関数２つくわせる
@function_list.add
def func1():
    print('func1 called')


@function_list.add
def func2():
    print('func2 called')

# 実行
function_list.run_all()
```

`func1()`を呼ぼうとすると`Pylance`は型推論の時点でNone型と推論されるため、無いことが認知出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/5ab91dd7-190a-7f40-160d-cb891e3258fe.png)

## まとめ

根本的にTypeScriptではデコレータがついたあとの型情報を推論できないという問題があるようです。
そのため、クラス宣言やメソッドにつけるデコレータによって返却値が異なる実装を行う事自体がよろしくないようです。

このあたりはGithubのissueにあがっているそうです。

https://github.com/microsoft/TypeScript/issues/4881

それでは

## 参考にした記事

https://zenn.dev/miruoon_892/articles/365675fa5343ed

https://zenn.dev/pixiv/articles/ab9a7d7f654a79?redirected=1