2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiitanがほしい人の一人アドカレAdvent Calendar 2024

Day 10

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

Posted at

読み飛ばしてください

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

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

調べるにあたった経緯

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

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

// 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コマンドであることがわかります。

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となっていて仕様が変わっているそうです。(英語読めないので読み飛ばしていましたが一番上に書いてます。)

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

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

の5種類です。

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

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

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

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で関数的としてデコレータ扱うのであれば、シングルトンパターンで実装するのが現実的でしょう。

// 前後でログを出力するデコレータ
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クラスを継承した無名クラスを返却します。
この時、無名クラスに新規のメソッドを付与するなどが出来ます。

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なのに型がわからないのは辛いよなぁ。と思いながら見てました。

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

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

# メソッドを追加するデコレータ
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デコレータの返却値を変えてみます。

// 前後でログを出力するデコレータ
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

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

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

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

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

# 関数をいっぱい保持するクラス
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()

# 関数2つくわせる
@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

まとめ

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

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

それでは

参考にした記事

2
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?