routing-controllers
はexpress
のルーティングやパラメータの取得などをアノテーションをつけることで理解しやすい形で実装できるものになっています。
しかし、routing-controllers
はじめとするtypestack
のライブラリ群は、reflect-metadata
による型定義から情報を取得する仕組みに依存しているため、esbuildでビルドする際に、型定義情報が失われて、うまく機能しない部分があります。
特に私が遭遇したのは、本番環境でだけ@QueryParams
(Get時のクエリパラメーターをまとめて取得するアノテーション)が以下のようなエラーを起こすというものでした。
TypeError: Cannot read properties of undefined (reading 'prototype')
at /var/task/index.js:2160:41822
at Array.map (<anonymous>)
at zse.normalizeParamValue (/var/task/index.js:2160:41721)
at zse.handle (/var/task/index.js:2160:40629)
at /var/task/index.js:2160:50776
at Array.map (<anonymous>)
at dae.executeAction (/var/task/index.js:2160:50747)
at /var/task/index.js:2160:50411
at c (/var/task/index.js:70:64056)
at Ag.handle_request (/var/task/index.js:39:3813)
esbuildでは、型定義による型変換は機能しない
まず、@QueryParams
でクエリパラメーターを型定義で指定した型のオブジェクトとして受け取る際に、どのように動いているのかを見たいと思います。
以下のコードは、クエリパラメーターがGetInputFormという型に変換して受け取ることが期待されるコントローラーの例です。
@JsonController('/')
class Controller {
@Get()
async get(@Req() req: Request, @QueryParams() params: GetInputForm) {
return params;
}
}
クエリパラメーターの変換は具体的には、パラメーターを対象の型に、再帰的に変換する、normalizeParamValueで行われています。この中で、変換先の型情報が、ParamType
となりますが、冒頭のエラーはこの中のparam.targetType.prototype
で起こってしまっています。
const ParamType: Function | undefined = (Reflect as any).getMetadata(
'design:type',
param.targetType.prototype,
key
);
そこで、param
の生成元を見ると、ParamMetadata.tsでは、targetType
をgetMetadata
から抽出していることがわかります。
if (args.explicitType) {
this.targetType = args.explicitType;
} else {
const ParamTypes = (Reflect as any).getMetadata('design:paramtypes', args.object, args.method);
if (typeof ParamTypes !== 'undefined') {
this.targetType = ParamTypes[args.index];
}
}
しかし、getMetadata
は、reflect-metadata
によるメソッドで、esbuildでは、これが機能しないため、ParamTypes
がundefinedとなり、結果として、targetType
がundefinedとなります。
typestackが想定している使い方をしていればParamTypes
がundefinedとなることはないため、今回のようなエラーは起こらないですが、esbuildでは何らかの対処が必要です。
暫定対処
上に挙げたコードで、targetType
は、args.explicitType
によっても指定することができることがわかります。args
はもともと、@QueryParams()
使用時に指定するオプションで、以下のようにコントローラーを書くことによって、冒頭のエラーは回避することができます。
@JsonController('/')
class Controller {
@Get()
async get(
@Req() req: Request,
// 明示的にtypeを指定する
@QueryParams({ type: EventInputForm }) params: EventInputForm
) {
return params;
}
}
ただし、routing-controllers
はじめとする、typestackのプロジェクトは、型定義に基づいたユーティリティを提供する設計思想だと思うので、reflect-metadata
を無視せざるを得ない回避策はあまり得策ではない可能性があります。
そもそも、esbuildはtsconfigのemitDecoratorMetadata
を意図的にサポートしていないので、esbuildとtypestackプロジェクトの相性が良くないのだと思います。swcではサポートしているらしいので、コンパイラを変えるなどを検討する方が良いかもしれません。
コメント