読み飛ばしてください
どうも限界派遣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を返してみる
};
}
これを先程のメソッドに付与してみます。
返却値は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型と推論されるため、無いことが認知出来ます。
まとめ
根本的にTypeScriptではデコレータがついたあとの型情報を推論できないという問題があるようです。
そのため、クラス宣言やメソッドにつけるデコレータによって返却値が異なる実装を行う事自体がよろしくないようです。
このあたりはGithubのissueにあがっているそうです。
それでは
参考にした記事