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ではサポートしているらしいので、コンパイラを変えるなどを検討する方が良いかもしれません。


コメント