リフレクション
型情報自体を直接扱うには、基本的に2つの方式があります: Type オブジェクトと Reflection クラス。Type オブジェクトは typeOf<T>()
が返す通常の JS オブジェクトです。Reflection クラスについては以下で説明します。
typeOf
関数は、Interface、オブジェクトリテラル、Class、Function、型エイリアスを含むすべての型で動作します。型に関するすべての情報を含む Type オブジェクトを返します。ジェネリックを含め、任意の型を型引数として渡すことができます。
import { typeOf } from '@deepkit/type'; typeOf<string>(); //{kind: 5} typeOf<number>(); //{kind: 6} typeOf<{id: number}>(); //{kind: 4, types: [{kind: 6, name: 'id'}]} class User { id: number } typeOf<User>(); //{kind: 4, types: [...]} function test(id: number): string {} typeOf<typeof test>(); //{kind: 12, parameters: [...], return: {kind: 5}}
Type オブジェクトは、Type オブジェクトの型を示す kind
プロパティを持つ単純なオブジェクトリテラルです。kind
プロパティは数値で、enum ReflectionKind
の値に対応します。ReflectionKind
は @deepkit/type
パッケージで次のように定義されています:
enum ReflectionKind { never, //0 any, //1 unknown, //2 void, //3 object, //4 string, //5 number, //6 boolean, //7 symbol, //8 bigint, //9 null, //10 undefined, //11 //... さらに多数 }
返される可能性のある Type オブジェクトはいくつかあります。最も単純なものは never
, any
, unknown
, void, null,
および undefined
で、次のように表現されます:
{kind: 0}; //never {kind: 1}; //any {kind: 2}; //unknown {kind: 3}; //void {kind: 10}; //null {kind: 11}; //undefined
たとえば、数値 0 は ReflectionKind
enum の最初のエントリ、つまり never
を表し、数値 1 は 2 番目のエントリ、つまり any
を表す、といった具合です。これに応じて、string
、number
、boolean
のようなプリミティブ型は次のように表現されます:
typeOf<string>(); //{kind: 5} typeOf<number>(); //{kind: 6} typeOf<boolean>(); //{kind: 7}
これらの比較的単純な型は、typeOf
に直接型引数として渡されているため、Type オブジェクトにはそれ以上の情報はありません。しかし、型エイリアス経由で型が渡される場合、Type オブジェクトには追加情報が含まれます。
type Title = string; typeOf<Title>(); //{kind: 5, typeName: 'Title'}
この場合、型エイリアス名 'Title' も取得できます。型エイリアスがジェネリックの場合、渡された型も Type オブジェクトに保持されます。
type Title<T> = T extends true ? string : number; typeOf<Title<true>>(); {kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]}
渡された型がインデックスアクセス演算子の結果である場合、コンテナとインデックスタイプも保持されます:
interface User { id: number; username: string; } typeOf<User['username']>(); {kind: 5, indexAccessOrigin: { container: {kind: Reflection.objectLiteral, types: [...]}, Index: {kind: Reflection.literal, literal: 'username'} }}
Interface とオブジェクトリテラルはどちらも Reflection.objectLiteral として出力され、types
配列に Property と Method を含みます。
interface User { id: number; username: string; login(password: string): void; } typeOf<User>(); { kind: Reflection.objectLiteral, types: [ {kind: Reflection.propertySignature, name: 'id', type: {kind: 6}}, {kind: Reflection.propertySignature, name: 'username', type: {kind: 5}}, {kind: Reflection.methodSignature, name: 'login', parameters: [ {kind: Reflection.parameter, name: 'password', type: {kind: 5}} ], return: {kind: 3}}, ] } type User = { id: number; username: string; login(password: string): void; } typeOf<User>(); //上と同じオブジェクトを返します
インデックスシグネチャも types
配列に含まれます。
interface BagOfNumbers { [name: string]: number; } typeOf<BagOfNumbers>; { kind: Reflection.objectLiteral, types: [ { kind: Reflection.indexSignature, index: {kind: 5}, //string type: {kind: 6}, //number } ] } type BagOfNumbers = { [name: string]: number; } typeOf<BagOfNumbers>(); //上と同じオブジェクトを返します
Class はオブジェクトリテラルと似ており、classType
(Class 自体への参照)に加えて、types
配列の下に Property と Method を持ちます。
class User { id: number = 0; username: string = ''; login(password: string): void { //何もしない } } typeOf<User>(); { kind: Reflection.class, classType: User, types: [ {kind: Reflection.property, name: 'id', type: {kind: 6}}, {kind: Reflection.property, name: 'username', type: {kind: 5}}, {kind: Reflection.method, name: 'login', parameters: [ {kind: Reflection.parameter, name: 'password', type: {kind: 5}} ], return: {kind: 3}}, ] }
Reflection.propertySignature の種別が Reflection.property に、Reflection.methodSignature が Reflection.method に変わっていることに注意してください。Class 上の Property と Method には追加の属性があるため、この情報も取得できます。後者には visibility
、abstract
、default
などが含まれます。
Class の Type オブジェクトには、その Class 自身の Property と Method のみが含まれ、スーパークラスのものは含まれません。これは、インターフェイスやオブジェクトリテラルの Type オブジェクトとは対照的で、そちらではすべての親の PropertySignature と MethodSignature が解決されて types
に含まれます。スーパークラスの Property と Method を解決するには、ReflectionClass とその ReflectionClass.getProperties()
(後続のセクション参照)または @deepkit/type
の resolveTypeMembers()
を使用できます。
Type オブジェクトには非常に多くの種類があります。たとえば、literal、テンプレートリテラル、promise、enum、union、array、tuple などです。どれが存在し、どの情報が利用できるかを知るには、@deepkit/type
から Type
を import することをお勧めします。これは TypeAny、TypeUnknonwn、TypeVoid、TypeString、TypeNumber、TypeObjectLiteral、TypeArray、TypeClass など、すべての可能なサブタイプを含む union
です。そこに正確な構造が記されています。
Type キャッシュ
ジェネリック引数が渡されない限り、型エイリアス、Function、Class の Type オブジェクトはキャッシュされます。つまり、typeOf<MyClass>()
の呼び出しは常に同じオブジェクトを返します。
type MyType = string; typeOf<MyType>() === typeOf<MyType>(); //true
しかし、ジェネリック型が使用されると、渡される型が常に同じであっても、常に新しいオブジェクトが作成されます。これは、理論上無限の組み合わせが可能であり、そのようなキャッシュは事実上メモリリークになるためです。
type MyType<T> = T; typeOf<MyType<string>>() === typeOf<MyType<string>>(); //false
ただし、再帰的な型の中で同じ型が複数回インスタンス化される場合、その間はキャッシュされます。ただし、そのキャッシュの存続期間は型が計算されている間に限られ、その後は存在しません。また、Type オブジェクト自体はキャッシュされますが、新しい参照が返され、全く同一のオブジェクトではありません。
type MyType<T> = T; type Object = { a: MyType<string>; b: MyType<string>; }; typeOf<Object>();
Object
が計算されている間、MyType<string>
はキャッシュされます。したがって a
と b
の PropertySignature はキャッシュから同じ type
を持ちますが、同一の Type オブジェクトではありません。
ルート以外のすべての Type オブジェクトには parent プロパティがあり、通常は外側の親を指します。これは、たとえばある Type が union の一部かどうかを判断するのに役立ちます。
type ID = string | number; typeOf<ID>(); *Ref 1* { kind: ReflectionKind.union, types: [ {kind: ReflectionKind.string, parent: *Ref 1* } } {kind: ReflectionKind.number, parent: *Ref 1* } ] }
'Ref 1' は実際の union の Type オブジェクトを指します。
上記のようにキャッシュされた Type オブジェクトでは、parent
プロパティが常に本来の親を指しているとは限りません。たとえば、ある Class が複数回使用される場合、types
内の直近の型(TypePropertySignature および TypeMethodSignature)は正しい TypeClass を指しますが、これらのシグネチャ型の type
はキャッシュされたエントリの TypeClass のシグネチャ型を指します。親構造を無限に辿らず直近の親のみを読むために、これは重要な知識です。parent が無限の精度を持たないのは、パフォーマンス上の理由によるものです。
JIT キャッシュ
以降では、しばしば Type オブジェクトに基づく関数や機能について説明します。その一部を高性能に実装するため、Type オブジェクトごとの JIT(just in time)キャッシュが必要になります。これは getJitContainer(type)
を介して提供できます。この関数は任意のデータを保存できる単純なオブジェクトを返します。そのオブジェクトへの参照が保持されない限り、Type オブジェクト自体が参照されなくなった時点で GC によって自動的に削除されます。
Reflection クラス
typeOf<>()
関数に加えて、Type オブジェクトに対する OOP 代替を提供するさまざまなリフレクション用クラスがあります。これらのリフレクション用クラスは Class、インターフェイス/オブジェクトリテラル、および Function とその直接のサブタイプ(Properties, Methods, Parameters)に対してのみ利用できます。より深い型は、再び Type オブジェクトで読み取る必要があります。
import { ReflectionClass } from '@deepkit/type'; interface User { id: number; username: string; } const reflection = ReflectionClass.from<User>(); reflection.getProperties(); //[ReflectionProperty, ReflectionProperty] reflection.getProperty('id'); //ReflectionProperty reflection.getProperty('id').name; //'id' reflection.getProperty('id').type; //{kind: ReflectionKind.number} reflection.getProperty('id').isOptional(); //false
型情報の受け取り
型に対して動作する関数を提供するために、ユーザーに型を手動で渡してもらえるようにするのが有用な場合があります。たとえばバリデーション関数では、要求する型を最初の型引数として、検証対象のデータを最初の関数引数として渡せるようにすると便利です。
validate<string>(1234);
この関数が string
型を取得できるようにするには、そのことを型コンパイラに伝える必要があります。
function validate<T>(data: any, type?: ReceiveType<T>): void;
最初の型引数 T
への参照を持つ ReceiveType
は、validate
の各呼び出しで(type
が第2引数として宣言されているため)その型を2番目の位置に配置するよう、型コンパイラに指示します。実行時にその情報を読み出すには、resolveReceiveType
関数を使用します。
import { resolveReceiveType, ReceiveType } from '@deepkit/type'; function validate<T>(data: any, type?: ReceiveType<T>): void { type = resolveReceiveType(type); }
不要に新しい変数を作らないよう、結果を同じ変数に代入するのが有用です。type
には、型引数が渡されなかった、Deepkit の型コンパイラが正しくインストールされていない、もしくは型情報の出力が有効化されていない(上記のインストールのセクションを参照)といった場合に、Type オブジェクトが格納されるか、あるいは Error がスローされます。