Reflection
Um direkt mit den Typinformationen selbst zu arbeiten, gibt es zwei grundlegende Varianten: Type-Objekte und Reflection-Klassen. Type-Objekte sind reguläre JS-Objekte, die von typeOf<T>()
zurückgegeben werden. Reflection-Klassen werden unten besprochen.
Die Function typeOf
funktioniert für alle Types, einschließlich Interfaces, object literals, Classes, Functions und Type Aliases. Sie gibt ein Type-Objekt zurück, das alle Informationen über den Type enthält. Du kannst jeden Type als Type Argument übergeben, einschließlich Generics.
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}}
Das Type-Objekt ist ein einfaches object literal, das eine Property kind
enthält, die den Typ des Type-Objekts angibt. Die Property kind
ist eine Zahl und erhält ihre Bedeutung aus dem Enum ReflectionKind
. ReflectionKind
ist im Package @deepkit/type
wie folgt definiert:
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 //... und noch mehr }
Es gibt eine Reihe möglicher Type-Objekte, die zurückgegeben werden können. Die einfachsten sind never
, any
, unknown
, void, null,
und undefined
, die wie folgt dargestellt werden:
{kind: 0}; //never {kind: 1}; //any {kind: 2}; //unknown {kind: 3}; //void {kind: 10}; //null {kind: 11}; //undefined
Beispielsweise ist die Zahl 0 der erste Eintrag des ReflectionKind
-Enums, in diesem Fall never
, die Zahl 1 ist der zweite Eintrag, hier any
, und so weiter. Dementsprechend werden primitive Types wie string
, number
, boolean
wie folgt dargestellt:
typeOf<string>(); //{kind: 5} typeOf<number>(); //{kind: 6} typeOf<boolean>(); //{kind: 7}
Diese eher einfachen Types haben keine weiteren Informationen am Type-Objekt, da sie direkt als Type Argument an typeOf
übergeben wurden. Wenn Types jedoch über Type Aliases übergeben werden, sind zusätzliche Informationen am Type-Objekt zu finden.
type Title = string; typeOf<Title>(); //{kind: 5, typeName: 'Title'}
In diesem Fall ist auch der Name des Type Alias 'Title' verfügbar. Wenn ein Type Alias ein Generic ist, sind die übergebenen Types ebenfalls am Type-Objekt verfügbar.
type Title<T> = T extends true ? string : number; typeOf<Title<true>>(); {kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]}
Wenn der übergebene Type das Ergebnis eines Index Access Operators ist, sind der Container und der Index Type vorhanden:
interface User { id: number; username: string; } typeOf<User['username']>(); {kind: 5, indexAccessOrigin: { container: {kind: Reflection.objectLiteral, types: [...]}, Index: {kind: Reflection.literal, literal: 'username'} }}
Interfaces und object literals werden beide als Reflection.objectLiteral ausgegeben und enthalten die Properties und Methods im Array types
.
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>(); //gibt dasselbe Objekt wie oben zurück
Index Signatures befinden sich ebenfalls im Array 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>(); //gibt dasselbe Objekt wie oben zurück
Classes sind ähnlich wie object literals und haben zusätzlich zu classType
, welches eine Referenz auf die Class selbst ist, ihre Properties und Methods unter einem types
-Array.
class User { id: number = 0; username: string = ''; login(password: string): void { //nichts tun } } 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}}, ] }
Beachte, dass sich der Type von Reflection.propertySignature zu Reflection.property und Reflection.methodSignature zu Reflection.method geändert hat. Da Properties und Methods auf Classes zusätzliche Attribute haben, können diese Informationen ebenfalls abgefragt werden. Letztere enthalten zusätzlich visibility
, abstract
und default
.
Type-Objekte von Classes enthalten nur die Properties und Methods der Class selbst und nicht der Super-Classes. Dies steht im Gegensatz zu Type-Objekten von Interfaces/object literals, die alle Property Signatures und Method Signatures aller Parents im types
-Array aufgelöst haben. Um die Properties und Methods der Super-Classes aufzulösen, können entweder ReflectionClass und dessen ReflectionClass.getProperties()
(siehe folgende Abschnitte) oder resolveTypeMembers()
aus @deepkit/type
verwendet werden.
Es gibt eine ganze Fülle an Type-Objekten. Zum Beispiel für literal, template literals, promise, enum, union, array, tuple und viele mehr. Um herauszufinden, welche es alle gibt und welche Informationen verfügbar sind, wird empfohlen, Type
aus @deepkit/type
zu importieren. Es ist ein union
mit allen möglichen Subtypes wie TypeAny, TypeUnknonwn, TypeVoid, TypeString, TypeNumber, TypeObjectLiteral, TypeArray, TypeClass und vielen mehr. Dort findest du die genaue Struktur.
Type Cache
Type-Objekte werden für Type Aliases, Functions und Classes gecacht, sobald kein Generic-Argument übergeben wird. Das bedeutet, ein Aufruf von typeOf<MyClass>()
gibt immer dasselbe Objekt zurück.
type MyType = string; typeOf<MyType>() === typeOf<MyType>(); //true
Sobald jedoch ein Generic Type verwendet wird, werden immer neue Objekte erstellt, selbst wenn der übergebene Type immer derselbe ist. Dies liegt daran, dass theoretisch eine unendliche Anzahl von Kombinationen möglich ist und ein solcher Cache effektiv ein Memory Leak wäre.
type MyType<T> = T; typeOf<MyType<string>>() === typeOf<MyType<string>>(); //false
Sobald ein Type jedoch in einem rekursiven Type mehrfach instanziiert wird, wird er gecacht. Die Dauer des Caches ist jedoch nur auf den Moment begrenzt, in dem der Type berechnet wird, und existiert danach nicht mehr. Außerdem wird zwar das Type-Objekt gecacht, es wird aber eine neue Referenz zurückgegeben und ist nicht exakt dasselbe Objekt.
type MyType<T> = T; type Object = { a: MyType<string>; b: MyType<string>; }; typeOf<Object>();
MyType<string>
wird so lange gecacht, wie Object
berechnet wird. Die PropertySignature von a
und b
haben somit denselben type
aus dem Cache, sind aber nicht dasselbe Type-Objekt.
Alle nicht-root Type-Objekte haben eine Property parent
, die üblicherweise auf den umschließenden Parent zeigt. Dies ist zum Beispiel wertvoll, um herauszufinden, ob ein Type Teil einer Union ist oder nicht.
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‘ zeigt auf das eigentliche Union Type-Objekt.
Bei gecachten Type-Objekten wie oben exemplarisch dargestellt, sind die parent
-Properties nicht immer die echten Parents. Beispielsweise bei einer Class, die mehrfach verwendet wird: Obwohl unmittelbare Types in types
(TypePropertySignature und TypeMethodSignature) auf die korrekte TypeClass zeigen, zeigen die type
-Properties dieser Signature Types auf die Signature Types der TypeClass des gecachten Eintrags. Das ist wichtig zu wissen, um nicht die Parent-Struktur unendlich zu traversieren, sondern nur den unmittelbaren Parent. Die Tatsache, dass der Parent keine unendliche Präzision hat, ist aus Performance-Gründen so.
JIT Cache
Im weiteren Verlauf werden einige Functions und Features beschrieben, die oft auf den Type-Objekten basieren. Um einige davon performant zu implementieren, wird ein JIT (just in time) Cache pro Type-Objekt benötigt. Dieser kann über getJitContainer(type)
bereitgestellt werden. Diese Function gibt ein einfaches Objekt zurück, auf dem beliebige Daten gespeichert werden können. Solange keine Referenz auf das Objekt gehalten wird, wird es automatisch vom GC gelöscht, sobald das Type-Objekt selbst ebenfalls nicht mehr referenziert wird.
Reflection-Klassen
Zusätzlich zur Function typeOf<>()
gibt es verschiedene Reflection-Klassen, die eine OOP-Alternative zu den Type-Objekten bieten. Die Reflection-Klassen sind nur für Classes, Interface/object literals und Functions sowie deren direkte Sub-Types (Properties, Methods, Parameters) verfügbar. Alle tieferen Types müssen wieder mit den Type-Objekten gelesen werden.
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
Typinformationen empfangen
Um Functions bereitzustellen, die auf Types operieren, kann es nützlich sein, dem Benutzer anzubieten, einen Type manuell zu übergeben. Zum Beispiel könnte es in einer Validierungsfunction sinnvoll sein, den anzufordernden Type als erstes Type Argument und die zu validierenden Daten als erstes Function Argument bereitzustellen.
validate<string>(1234);
Damit diese Function den Type string
erhält, muss sie dies dem Type Compiler mitteilen.
function validate<T>(data: any, type?: ReceiveType<T>): void;
ReceiveType
mit dem Verweis auf das erste Type Argument T
signalisiert dem Type Compiler, dass jeder Aufruf von validate
den Type an zweiter Stelle ablegen soll (da type
an zweiter Stelle deklariert ist). Um die Informationen zur Laufzeit auszulesen, wird anschließend die Function resolveReceiveType
verwendet.
import { resolveReceiveType, ReceiveType } from '@deepkit/type'; function validate<T>(data: any, type?: ReceiveType<T>): void { type = resolveReceiveType(type); }
Es ist sinnvoll, das Ergebnis derselben Variable zuzuweisen, um nicht unnötig eine neue zu erstellen. In type
ist nun entweder ein Type-Objekt gespeichert oder es wird ein Error geworfen, wenn z. B. kein Type Argument übergeben wurde, Deepkits Type Compiler nicht korrekt installiert wurde oder das Emittieren von Typinformationen nicht aktiviert ist (siehe den Abschnitt Installation oben).