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

More than 3 years have passed since last update.

TypeScriptの型宣言を実行時に取り出す

Posted at

背景

TypeScriptの型宣言(プロパティの型、引数や戻り値の型)を実行時に取り出すことはできるのかなと思って調べてみました。どうもできるようなのですが、出てくる情報を見てもいまいちピンときません。他言語のリフレクションのように

MyClass.getPropertyType('myProperty')  // => URL

みたいなAPIを想像していたのですが、なぜかデコレータの話が始まるのです。どういうことでしょうか。

結論

結論から言うと、上の想像に近いことは以下のようなコードで可能です:

import 'reflect-metadata';

// 何もしないデコレータ。
function property(target: any, propertyKey: string) {}

class MyClass {
  @property
  myProperty: URL;

  @property
  set myAccessor(value: URL) { this.myProperty = value; }
  get myAccessor() { return this.myProperty; }

  @property
  myMethod(a: number, b: string): number {
    return 0;
  }
}

console.log(Reflect.getMetadata('design:type', MyClass.prototype, 'myProperty'));
// => [class URL]

console.log(Reflect.getMetadata('design:type', MyClass.prototype, 'myAccessor'));
// => [class URL]

console.log(Reflect.getMetadata('design:returntype', MyClass.prototype, 'myMethod'));
// => [Function: Number]

console.log(Reflect.getMetadata('design:paramtypes', MyClass.prototype, 'myMethod'));
// => [ [Function: Number], [Function: String] ]

ちなみにこれはTypeScriptの実験的な機能なので、以下のような tsconfig.json が必要になります:

tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

reflect-metadata もyarnやnpmで入れておいてください。

解説

さて、結局デコレータが出てきました。上のコードの @property というやつがそれです。詳しくは説明しないので公式ドキュメント日本語訳)をどうぞ。しかしなぜデコレータが必要なのでしょうか。

まず背景として、TypeScriptの型宣言というのは基本的にコンパイル時の型チェックのためのもので、実行時には残りません。これを emitDecoratorMetadata を指定することで、 design:type などのメタデータとして実行時に残るようになります。メタデータのAPIはこちら

ただしこのメタデータが付くのは、何かのデコレータがついてるメンバ(またはクラス自身)だけのようです。というわけで、上の例では何もしない(空の)デコレータを付けました。これでも型宣言を取り出せるようになります。

このことから、制約として、(デコレータがついていない)既存のクラスの型宣言を取り出すことはできません。他にも以下のような制約があるようです。

  • ジェネリック型の型引数を取り出すことはできない。number[]と宣言されていても、取り出せるのはArrayだけです。

  • 引数名を取り出すことはできない。引数名を取り出したければ、デコレーションを使って別途引数名を残す必要がある。例:

    myMethod(@named('a') a: number, @named('b') b: string) { ... }
    
  • (メソッドではない)関数には適用できない。そもそも関数にはデコレータを付けられない。

クラスのメンバを列挙する

やや余談ですが、クラスのメンバ(プロパティやメソッド)を列挙しようとして気づいたのが、TypeScriptで宣言されただけの(ゲッタなどを持たない)プロパティを列挙する方法が(おそらく)ないということです。

class MyClass {
  myProperty: URL;  // こういうやつ
}

メソッドなどはMyClass.prototypeに入っているのですが、宣言されただけのプロパティは実行時の実体を持たないためです。

今回はどのみちデコレータを付ける必要があるので、デコレータの中でプロトタイプのメタデータにプロパティ名を記録すれば解決です。

function property(target: any, propertyKey: string) {
  const properties = Reflect.getOwnMetadata('custom:properties', target) ?? [];
  properties.push(propertyKey);
  Reflect.defineMetadata('custom:properties', properties, target);
}

全部入りのコード例

上に書いたことを全部入れたコード例がこちらです。与えられたクラスの各メンバの型宣言(引数名を含む)を出力する関数 dumpTypeDeclaration() を定義しています。

import 'reflect-metadata';

function property(target: any, propertyKey: string) {
  // プロトタイプのメタデータにプロパティを記録。
  const properties = Reflect.getOwnMetadata('custom:properties', target) ?? [];
  properties.push(propertyKey);
  Reflect.defineMetadata('custom:properties', properties, target);
}

// メタデータにパラメータ名を記録するデコレータ。
function named(name: string) {
  return (target: any, propertyKey: string, index: number) => {
    const paramNames =
      Reflect.getOwnMetadata('custom:paramnames', target, propertyKey) ?? [];
    paramNames[index] = name;
    Reflect.defineMetadata('custom:paramnames', paramNames, target, propertyKey);
  };
}

function dumpTypeDeclaration(klass: Function) {
  const propertyKeys = Reflect.getMetadata('custom:properties', klass.prototype);
  for (const propertyKey of propertyKeys) {
    const propertyType = Reflect.getMetadata('design:type', klass.prototype, propertyKey);
    const returnType = Reflect.getMetadata('design:returntype', klass.prototype, propertyKey);
    const paramTypes = Reflect.getMetadata('design:paramtypes', klass.prototype, propertyKey);
    const paramNames = Reflect.getMetadata('custom:paramnames', klass.prototype, propertyKey);
    console.log(`${propertyKey}: ${propertyType.name}`);
    if (paramTypes !== undefined && paramNames !== undefined) {
      console.log('  params:');
      for (let i = 0; i < paramTypes.length; ++i) {
        console.log(`    ${paramNames[i]}: ${paramTypes[i].name}`);
      }
    }
    if (returnType !== undefined) {
      console.log(`  returns: ${returnType.name}`);
    }
  }
}

class MyClass {
  @property
  myProperty: URL;

  @property
  set myAccessor(value: URL) { this.myProperty = value; }
  get myAccessor() { return this.myProperty; }

  @property
  myMethod(@named('a') a: number, @named('b') b: string): number {
    return 0;
  }
}

dumpTypeDeclaration(MyClass);

出力:

myProperty: URL
myAccessor: URL
myMethod: Function
  params:
    a: Number
    b: String
  returns: Number

感想

どうも制約が多くてちょっと使いにくい印象です。実験的な機能だからというのもあるでしょうか。

特に、デコレータがついてないとメタデータがつかない(逆に、何もしないメタデータでもついていればいい)というのはなんか妙な気がします。理由は知りませんが、 emitDecoratorMetadata という名前ですし、デコレータで使うことを想定された機能なんでしょうね。あらゆるクラスに型宣言のメタデータをつけるのは無駄すぎるというのもあるかもしれません。

参考

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