Runtime Types

Typeninformationen in TypeScript zur Laufzeit zur Verfügung zu stellen ändert vieles. Es erlaubt neue Arbeitsweisen, die zuvor nur über Umwege oder gar nicht möglich waren. Das Deklarieren von Typen und Schemas ist mittlerweile ein großer Teil moderner Entwicklungsprozessen geworden. So sind GraphQL, Validatoren, ORMs, und Encoder wie zum Beispiel ProtoBuf, und viele mehr darauf angewiesen, Schema-Informationen auch zur Laufzeit zur Verfügung zu haben, um so fundamentale Funktionalitäten überhaupt erst bereitstellen zu können. Diese Tools und Libraries verlangen vom Entwickler teilweise komplett neue Sprachen zu lernen, die sehr spezifisch für den Anwendungsfall entwickelt worden sind. So haben ProtoBuf und GraphQL ihre eigene Deklarationssprache, auch Validatoren basieren oft auf eigene Schema-APIs oder gar JSON-Schema, welches ebenfalls eine eigenständige Art ist, Strukturen zu definieren. Einige davon verlangen bei jeder Änderung das Ausführen von Code-Generatoren, um die Schema-Informationen auch der Laufzeit bereitzustellen. Ein anderes bekanntes Muster ist, experimentelle TypeScript Decorators zu verwenden, um Meta-Informationen an Klassen der Laufzeit zur Verfügung zu stellen.

Aber ist das alles nötig? TypeScript bietet eine sehr mächtige Sprache, um auch sehr komplexe Strukturen zu beschreiben. Tatsächlich ist TypeScript mittlerweile Touring-Complete, was grob bedeutet, dass theoretisch jede Art von Program in TypeScript abbildbar ist. Natürlich hat dies seine praktischen Grenzen, der wichtige Punkt ist jedoch, dass TypeScript in der Lage ist, jegliche Deklarationsformate wie GraphQL, ProtoBuf, JSON-Schema, und viele andere komplett zu ersetzen. In Kombination mit einem Typensystem zur Laufzeit ist es möglich, all die beschriebenen Tools und deren Anwendungsfälle in TypeScript selbst ohne jeglichen Code-Generator abzudecken. Warum gibt es aber noch keine Lösung, die genau dies erlaubt?

Historisch gesehen ist TypeScript in den letzten Jahren einem massiven Wandel unterzogen worden. Es wurde diverse male komplett neu geschrieben, hat grundlegende Features erhalten, und unterlief eine ganze Reihe von Iterationen und Breaking-Changes. Mittlerweile ist TypeScript jedoch an einem Produkt-Market-Fit angekommen, das die Geschwindigkeit, in der grundlegende Innovationen und Breaking-Changes passieren, stark verlangsamt. TypeScript hat sich bewährt und gezeigt, wie ein äußerst charmantes Typensystem für eine hochdynamische Sprache wie JavaScript auszusehen hat. Der Markt hat diesen Vorstoß dankend angenommen und eine neue Äre in der Entwicklung mit JavaScript eingeleitet.

Genau dann ist der richtige Zeitpunkt gekommen, Tools auf der Sprache selbst in fundamentaler Ebene aufzusetzen, um so das oben beschriebene möglich zu machen. Deepkit möchte der Anstoß sein, um über jahrzehnte bewährte Design-Muster aus dem Enterprise von Sprachen wie Java und PHP nicht nur fundamental zu TypeScript zu bringen, sondern in einer neuen und besseren Art, die nicht gegen, sondern mit JavaScript arbeitet. Durch Typeninformationen zur Laufzeit sind diese nun zum ersten Mal nicht nur prinzipiell möglich, sondern erlauben ganz neue viel einfacherer Design-Muster, die mit Sprachen wie Java und PHP nicht möglich sind. TypeScript selbst hat hier das Fundament gelegt, um mit ganz neue Ansätzen in starker Kombination mit Bewährtem dem Entwickler das Leben beträchtlich zu vereinfachen.

Typeninformationen zur Laufzeit auszulesen ist die Fähigkeit auf die Deepkit in seinem Fundament aufsetzt. Die API der Deepkit Libraries sind maßgeblich darauf ausgerichtet, soviel TypeScript Typeninformation wie möglich zu verwenden, um so effizient wie möglich zu sein. Typensystem zur Laufzeit bedeutet, dass Typeninformationen zur Laufzeit auslesbar und dynamische Typen berechenbar sind. Das heisst, dass zum Beispiel bei Klassen alle Eigenschaften und bei Funktionen alle Parameter und Return-Typen ausgelesen werden können.

Nehmen wir als Beispiel diese Funktion:

function log(message: string): void {
    console.log(message);
}

In JavaScript selbst können mehrere Informationen zu Laufzeit ausgelesen werden. Zum Beispiel der Name der Funktion (sofern nicht mit einem Minimizer abgeändert wurde):

log.name; //‘log’

Zum anderen kann die Anzahl der Parameter ausgelesen werden:

log.length; //1

Mit ein bisschen mehr Code kann auch ausgelesen werden, wie die Parameter heissen. Das ist jedoch ohne einen rudimentären JavaScript-Parser oder RegExp auf log.toString() nicht ohne weiteres zu bewerkstelligen, sodass ab hier schon schluss ist. Da TypeScript die obige Funktion wie folgt in JavaScript übersetzt:

function log(message) {
    console.log(message);
}

sind die Informationen, dass message vom Typ string und der Return-Typ vom Type void ist nicht mehr verfügbar. Diese Informationen wurden unwiderruflich von TypeScript zerstört.

Mit einem Typensystem zur Laufzeit können jedoch diese Informationen überleben, so dass man die Typen von message und den Return-Typ programmatisch auslesen kann.

import { typeOf, ReflectionKind } from '@deepkit/type';

const type = typeOf(log);
type.kind; //ReflectionKind.function
type.parameters[0].name; //'message'
type.parameters[0].type; //{kind: ReflectionKind.string}
type.return; //{kind: ReflectionKind.void}

Deepkit macht genau das möglich. Es hängt sich in die Kompilierung von TypeScript ein und stellt sicher, dass alle Typeninformationen in dem generierten JavaScript eingebaut sind. Funktionen wie typeOf() (nicht zu verwechseln mit dem operator typeof, mit kleinem o) erlauben dem Entwickler dann darauf zuzugreifen. Es können daher auch Libraries entwickelt werden, die auf diesen Typeninformationen basieren und so dem Entwickler es erlauben, bereits geschriebene TypeScript Typen für eine ganze Palette von Anwendungsmöglichkeiten zu verwenden.

Installation

Um Deepkit’s Runtime Typessystem zu installieren werden zwei Pakete benötigt. Der Typen-Compiler in @deepkit/type-compiler und die dazu nötige Runtime in @deepkit/type. Der Typen-Compiler kann dabei in package.json devDependencies installiert werden, da er nur zur Build-Zeit benötigt wird.

npm install --save @deepkit/type
npm install --save-dev @deepkit/type-compiler

Laufzeit Typeninformationen werden standardmäßig nicht generiert. Es muss "reflection": true in der Datei tsconfig.json gesetzt werden, um es in allen Dateien im gleichen Ordner dieser Datei oder in allen Unterordnern zu aktivieren. Wenn Decorators verwenden werden sollen, muss "experimentalDecorators": true in der tsconfig.json aktiviert werden. Dies ist nicht unbedingt erforderlich, um mit @deepkit/type zu arbeiten, aber für bestimmte Funktionen anderen Deepkit Libraries und in @deepkit/framework notwendig.

Datei: tsconfig.json

{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "es6",
    "moduleResolution": "node",
    "experimentalDecorators": true
  },
  "reflection": true
}

Type compiler

TypeScript selbst erlaubt es nicht, den Typen-Compiler über eine tsconfig.json zu konfigurieren. Es ist entweder nötig, die TypeScript Compiler API direkt oder ein Build-System wie Webpack mit ts-loader zu benutzen. Um diesen unangenehmen Weg den Benutzern von Deepkit zu ersparen, installiert sich der Deepkit Typen-Compiler automatisch selbst in node_modules/typescript sobald @deepkit/type-compiler installiert wird (dies geschieht über NPM install hooks). Dies macht es möglich, dass alle Buildtools, die auf das lokal installierte TypeScript (das in node_modules/typescript) zugreifen, automatisch den Typen-Compiler aktiviert haben. Dadurch funktioniert tsc, Angular, webpack, ts-node, und einige andere Tools automatisch mit dem Deepkit Typen-Compiler.

Falls der Typen-Compiler nicht erfolgreich automatisch installiert werden konnte (weil zum Beispiel NPM install hooks deaktiviert sind), kann dies manuell mit folgendem Kommando nachgeholt werden:

node_modules/.bin/deepkit-type-install

Beachten Sie, dass deepkit-type-install ausführt werden muss, wenn die lokale Typescript-Version aktualisiert wurde (zum Beispiel, wenn sich die Typescript-Version in package.json geändert hat und npm install ausgeführt wird).

Webpack

Wenn der Typen-Compiler in einem Webpack-Build verwenden werden solle, kann dies mit dem Paket ts-loader (oder jedem anderen Typescript-Loader, der die Registrierung von Transformatoren unterstützt) tun.

Datei: webpack.config.js

const typeCompiler = require('@deepkit/type-compiler');

module.exports = {
  entry: './app.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
          use: {
            loader: 'ts-loader',
            options: {
              //this enables @deepkit/type's type compiler
              getCustomTransformers: (program, getProgram) => ({
                before: [typeCompiler.transformer],
                afterDeclarations: [typeCompiler.declarationTransformer],
              }),
            }
          },
          exclude: /node_modules/,
       },
    ],
  },
}

Typen-Decorators

Typen-Decorators sind normale TypeScript-Typen, die Meta-Informationen beinhalten, um zur Laufzeit das Verhalten diverser Funktionen zu verändern. Deepkit liefert bereits einige Typen-Decorators mit, die einige Anwendungsfälle abdecken. So kann zum Beispiel eine Klassen-Eigenschaft als Primary-Key, als Referenz, oder Index markiert werden. Die Datenbank Library kann diese Information zur Laufzeit nutzen, um so die korrekten SQL Queries ohne vorherige Code-Generation zu erstellen. Es können auch Validator-Einschränkungen wie zum Beispiel MaxLength, Maximum, oder Positive an einen beliebigen Typen hinzugefügt werden. Auch kann dem Serializer mitgeteilt werden, wie ein bestimmter Wert zu serialisieren bzw deserialisieren ist. Zusätzlich ist es möglich, komplett eigene Type-Decorators zu erstellen und zur Laufzeit auszulesen, um so sehr individuell das Typensystem zur Laufzeit zu verwenden.

Deepkit kommt mit einer ganzen Reihe von Typen-Decorators, die alle direkt aus @deepkit/type benutzt werden können. Sie sind designt, nicht aus mehreren Libraries zu kommen, um so Code nicht direkt an eine bestimmte Library wie zum Beispiel Deepkit RPC oder Deepkit Database zu koppeln. Das erlaubt das einfachere Wiederverwenden von Typen, auch im Frontend, obwohl zum Beispiel Datenbank Typen-Decorators genutzt werden.

Folgend ist eine Liste von vorhandenen Type-Decorators. Der Validator und Serializer von @deepkit/type und @deepkit/bson sowie Deepkit Database von @deepkit/orm nutzten diese Informationen unterschiedlich. Siehe die entsprechenden Kapitel, um mehr darüber zu erfahren.

Integer/Float

Integer und Floats sind als Basis als number definiert und hat mehrere Untervarianten:

Type Description

integer

An integer of arbitrary size.

int8

An integer between -128 and 127.

uint8

An integer between 0 and 255.

int16

An integer between -32768 and 32767.

uint16

An integer between 0 and 65535.

int32

An integer between -2147483648 and 2147483647.

uint32

An integer between 0 and 4294967295.

float

Same as number, but might have different meaning in database context.

float32

A float between -3.40282347e+38 and 3.40282347e+38. Note that JavaScript is not able to check correctly the range due to precision issues, but the information might be handy for the database or binary serializers.

float64

Same as number, but might have different meaning in database context.

import { integer } from '@deepkit/type';

interface User {
    id: integer;
}

Hier ist zur Laufzeit die id des Users eine Number, wird jedoch in der Validierung und Serialisierung als Integer interpretiert. Das heisst, dass hier zum Beispiel keine Floats in Validation genutzt werden dürfen und der Serializer Floats automatisch in Integer umwandeln.

import { is, integer } from '@deepkit/type';

is<integer>(12); //true
is<integer>(12.5); //false

Die Untertypen können genauso benutzt werden und sind sinnvoll, wenn ein bestimmter Nummernbereich erlaubt werden soll.

import { is, int8 } from '@deepkit/type';

is<int8>(-5); //true
is<int8>(5); //true
is<int8>(-200); //false
is<int8>(2500); //false

Float

UUID

UUID v4 wird in der Datenbank in der Regel als Binary abgespeichert und in JSON als String.

import { is, UUID } from '@deepkit/type';

is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true
is<UUID>('asd'); //false

MongoID

Marks this field as ObjectId for MongoDB. Resolves as a string. Is stored in the MongoDB as binary.

import { MongoId, serialize, is } from '@deepkit/type';

serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011
is<MongoId>('507f1f77bcf86cd799439011'); //true
is<MongoId>('507f1f77bcf86cd799439011'); //false

class User {
    id: MongoId = ''; //will automatically set in Deepkit ORM once user is inserted
}

Bigint

Per default the normal bigint type serializes as number in JSON (and long in BSON). This has however limitation in what is possible to save since bigint in JavaScript has an unlimited potential size, where numbers in JavaScript and long in BSON are limited. To bypass this limitation the types BinaryBigInt and SignedBinaryBigInt are available.

BinaryBigInt is the same as bigint but serializes to unsigned binary with unlimited size (instead of 8 bytes in most databases) in databases and string in JSON. Negative values will be converted to positive (abs(x)).

import { BinaryBigInt } from '@deepkit/type';

interface User {
    id: BinaryBigInt;
}

const user: User = {id: 24n};

serialize<User>({id: 24n}); //{id: '24'}

serialize<BinaryBigInt>(24); //'24'
serialize<BinaryBigInt>(-24); //'0'

Deepkit ORM stores BinaryBigInt as a binary field.

SignedBinaryBigInt is the same as BinaryBigInt but is able to store negative values as well. Deepkit ORM stores SignedBinaryBigInt as binary. The binary has an additional leading sign byte and is represented as an uint: 255 for negative, 0 for zero, or 1 for positive.

import { SignedBinaryBigInt } from '@deepkit/type';

interface User {
    id: SignedBinaryBigInt;
}

MapName

To change the name of a property in the serialization.

import { serialize, deserialize, MapName } from '@deepkit/type';

interface User {
    firstName: string & MapName<'first_name'>;
}

serialize<User>({firstName: 'Peter'}) // {first_name: 'Peter'}
deserialize<User>({first_name: 'Peter'}) // {firstName: 'Peter'}

Group

Properties can be grouped together. For serialization you can for example exclude a group from serialization. See the chapter Serialization for more information.

import { serialize } from '@deepkit/type';

interface Model {
    username: string;
    password: string & Group<'secret'>
}

serialize<Model>(
    { username: 'Peter', password: 'nope' },
    { groupsExclude: ['secret'] }
); //{username: 'Peter'}

Data

Each property can add additional meta-data that can be read via the Reflection API. See Runtime Types Reflection for more information.

import { ReflectionClass } from '@deepkit/type';

interface Model {
    username: string;
    title: string & Data<'key', 'value'>
}

const reflection = ReflectionClass.from<Model>();
reflection.getProperty('title').getData()['key']; //value;

Excluded

Each property can be excluded from the serialization process for a specific target.

import { serialize, deserialize, Excluded } from '@deepkit/type';

interface Auth {
    title: string;
    password: string & Excluded<'json'>
}

const item = deserialize<Auth>({title: 'Peter', password: 'secret'});

item.password; //undefined, since deserialize's default serializer is called `json`

item.password = 'secret';

const json = serialize<Auth>(item);
json.password; //again undefined, since serialize's serializer is called `json`

Embedded

Marks the field as an embedded type.

import { PrimaryKey, Embedded, serialize, deserialize } from '@deepkit/type';

interface Address {
    street: string;
    postalCode: string;
    city: string;
    country: string;
}

interface User  {
    id: number & PrimaryKey;
    address: Embedded<Address>;
}

const user: User {
    id: 12,
    address: {
        street: 'abc', postalCode: '1234', city: 'Hamburg', country: 'Germany'
    }
};

serialize<User>(user);
{
    id: 12,
    address_street: 'abc',
    address_postalCode: '1234',
    address_city: 'Hamburg',
    address_country: 'Germany'
}

//for deserialize you have to provide the embedded structure
deserialize<User>({
    id: 12,
    address_street: 'abc',
    //...
});

It’s possible to change the prefix (which is per default the property name).

interface User  {
    id: number & PrimaryKey;
    address: Embedded<Address, {prefix: 'addr_'}>;
}

serialize<User>(user);
{
    id: 12,
    addr_street: 'abc',
    addr_postalCode: '1234',
}

//or remove it entirely
interface User  {
    id: number & PrimaryKey;
    address: Embedded<Address, {prefix: ''}>;
}

serialize<User>(user);
{
    id: 12,
    street: 'abc',
    postalCode: '1234',
}

Entity

To annotate interfaces with entity information. Only used in the database context.

import { Entity, PrimaryKey } from '@deepkit/type';

interface User extends Entity<{name: 'user', collection: 'users'> {
    id: number & PrimaryKey;
    username: string;
}

InlineRuntimeType

TODO

ResetDecorator

TODO

Database

TODO: PrimaryKey, AutoIncrement, Reference, BackReference, Index, Unique, DatabaseField.

Validation

TODO

Benutzerdefinierte Type-Decorators

Ein Typen-Decorator kann wie folgt definiert werden:

type MyAnnotation = {__meta?: ['myAnnotation']};

Als Konvention ist definiert, dass ein Typen-Decorator ein Object-Literal mit einem einzigen optionalen Property __meta ist, das ein Tuple als Typ hat. Der erste Eintrag in diesem Tuple ist sein eindeutiger Name und alle weiteren Tuple Einträge beliebige Optionen. So kann ein Typen-Decorator mit zusätzlichen Optionen ausgestattet werden.

type AnnotationOption<T extends {title: string}> = {__meta?: ['myAnnotation', T]};

Genutzt wird der Typen-Decorator mit dem Intersection-Operator &. Es können beliebig viele Typen-Decorators an einem Typen genutzt werden.

type Username = string & MyAnnotation;
type Title = string & & MyAnnotation & AnnotationOption<{title: 'Hello'}>;

Ausgelesen können die Typen-Decorators über die Typen-Objekte von typeOf<T>() und metaAnnotation:

import { typeOf, metaAnnotation } from '@deepkit/type';

const type = typeOf<Username>();
const annotation = metaAnnotation.getForName(type, 'myAnnotation'); //[]

Das Resultat in annotation ist entweder ein Array mit Optionen, wenn der Typen-Decorator myAnnotation genutzt wurde oder undefined wenn nicht. Hat der Typen-Decorator zusätzliche Optionen wie in AnnotationOption zu sehen, sind die übergebenen Werte in dem Array zu finden. Bereits mitgelieferte Typen-Decorators wie MapName, Group, Data, etc haben ihre eigenen Annotation-Objekt:

import { typeOf, Group, groupAnnotation } from '@deepkit/type';
type Username = string & Group<'a'> & Group<'b'>;

const type = typeOf<Username>();
groupAnnotation.getAnnotations(type); //['a', 'b']

Siehe Runtime Types Reflection, um mehr darüber zu erfahren.

External Classes

Since TypeScript does not include type information per default, imported types/classes from other packages (that did not use @deepkit/type-compiler) will not have type information available.

To annotate types for an external class, use annotateClass and make sure this function is executed in the bootstrap phase of your application before the imported class is used somewhere else.

import { MyExternalClass } from 'external-package';
import { annotateClass } from '@deepkit/type';

interface AnnotatedClass {
    id: number;
    title: string;
}

annotateClass<AnnotatedClass>(MyExternalClass);

//all uses of MyExternalClass return now the type of AnnotatedClass
serialize<MyExternalClass>({...});

//MyExternalClass can now also be used in other types
interface User {
    id: number;
    clazz: MyExternalClass;
}

MyExternalClass can now be used in serialization functions and in the reflection API.

To following shows how to annotate generic classes:

import { MyExternalClass } from 'external-package';
import { annotateClass } from '@deepkit/type';

class AnnotatedClass<T> {
    id!: T;
}

annotateClass(ExternalClass, AnnotatedClass);

Reflection

Um mit den Typeninformationen selbst direkt zu arbeiten, gibt es dabei zwei grundlegende Varianten: Type-Objekte und Reflection-Klassen. Die Reflection-Klassen werden weiter unten behandelt. Die Funktion typeOf gibt Typen-Objekte zurück, die ganz simple object literals sind. Es beinhaltet immer ein kind welches eine Nummer ist und mittels dem Enum ReflectionKind seine Bedeutung erlangt. ReflectionKind ist in dem Paket @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

  //... and even more
}

Es gibt eine ganze Reihe von möglichen Typen-Objekten, die zurückgegeben werden können. Die einfachsten sind dabei never, any, unknown, void, null, und undefined, welche wie folgt dargestellt werden:

{kind: 0}; //never
{kind: 1}; //any
{kind: 2}; //unknown
{kind: 3}; //void
{kind: 10}; //null
{kind: 11}; //undefined

Die Nummer 0 zum Beispiel ist der erste Eintrag des ReflectionKind Enums, in diesem Fall never, die Nummer 1 der zweite Eintrag, hier any, und so weiter. Entsprechend sind primitive Typen wie string, number, boolean wie folgt dargestellt:

typeOf<string>(); //{kind: 5}
typeOf<number>(); //{kind: 6}
typeOf<boolean>(); //{kind: 7}

Diese recht simplen Typen haben keine weiteren Informationen an dem Typen-Objekt, da sie direkt als Typen-Argument zu typeOf übergeben wurden. Werden jedoch Typen über Typen-Aliase übergeben, sind zusätzliche Informationen an dem Typen-Objekt zu finden.

type Title = string;

typeOf<Title>(); //{kind: 5, typeName: 'Title'}

In diesem Fall ist der Name des Type-Alias Title ebenfalls vorhanden. Ist ein Type-Alias ein Generic, werden die übergebenen Typen ebenfalls an dem Typen-Objekt verfügbar.

type Title<T> = T extends true ? string : number;

typeOf<Title<true>>();
{kind: 5, typeName: 'Title', typeArguments: [{kind: 7}]}

Ist der übergebene Type das Ergebnis eines Index-Access Operators, ist 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 sind beide als Reflection.objectLiteral ausgegeben und beinhalten die Properties und Methoden in dem types array.

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>(); //returns the same object as above

Index Signatures sind ebenfalls in dem types array.

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>(); //returns the same object as above

Klassen sind ähnliche zu Object Literals und haben ihre Properties und Methods ebenfalls unter einem types array zusätzlich zu classType welches eine Referenz auf die Klasse selbst ist.

class User {
  id: number = 0;
  username: string = '';
  login(password: string): void {
     //do nothing
  }
}

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 der Type von Reflection.propertySignature zu Reflection.property und Reflection.methodSignature zu Reflection.method geändert wurde. Da Properties und Methoden an Klassen zusätzliche Attribute aufweisen, sind diese Informationen ebenfalls abrufbar. Letztere beinhalten zusätzlich visibility, abstract, und default. Typen-Objekte von Klassen beinhalten nur die Properties und Methoden der Klasse selbst und nicht der Super-Klassen. Das ist konträr zu Typen-Objekten von interfaces/object-literals, welche alle property signatures und method signatures aller Elternteile aufgelöst in types haben. Um die Property und Methoden der Super-Klassen aufzulösen, kann entweder ReflectionClass und dessen ReflectionClass.getProperties() (siehe nachfolgende Abschnitte) oder resolveTypeMembers() von @deepkit/type genutzt werden.

Es gibt eine ganze Hülle und Fülle von Typen-Objekten. So zum Beispiel für literal, template literals, promise, enum, union, array, tuple, und viele mehr. Um herauszufinden, welche es alle gibt und welche Informationen bereitstehen, empfiehlt es sich Type von @deepkit/type zu importieren. Es ist ein union mit allen Möglichen Subtypes wie z.b. TypeAny, TypeUnknonwn, TypeVoid, TypeString, TypeNumber, TypeObjectLiteral, TypeArray, TypeClass, und viele mehr. Dort ist dann die genaue Struktur zu finden.

Type Cache

Type-Objekte sind für Type-Aliase, Funktionen, und Klassen gecached sobald keine Generic-Argument übergeben ist. Das heisst konkret, dass ein Aufruf zu typeOf<MyClass>() immer das selbe Objekt zurückgibt.

type MyType = string;

typeOf<MyType>() === typeOf<MyType>(); //true

Sobald jedoch eine Generic-Type benutzt wird, werden immer neue Objekte erzeugt, selbst wenn der übergebene Typen immer dasselbe ist. Das ist so, da theoretisch unendlich viele Kombinationen möglich sind und so ein Cache effektiv ein Memory-Leak darstellen würde.

type MyType<T> = T;

typeOf<MyType<string>>() === typeOf<MyType<string>>();
//false

Sobald ein Typ jedoch in einen rekursiven Typen mehrfach instantiiert wird, ist dieser gecacht. Die Dauer des Cache ist allerdings nur auf den Moment der Berechnung des Types limitiert und ist danach nicht mehr existent. Auch ist zwar das Type-Objekt gecacht, doch wird eine neue Referenz zurückgegeben und ist nicht das exakt selbe Objekt.

type MyType<T> = T;
type Object = {
   a: MyType<string>;
   b: MyType<string>;
};

typeOf<Object>();

MyType<string> ist gecacht solange Object berechnet wird. Die PropertySignature von a und b haben dadurch zwar denselben type aus dem Cache, sind jedoch nicht dasselbe Type-Objekt.

Alle nicht-root Type-Objekte haben eine parent Eigenschaft, welche in der Regel auf den umschließenden Elternteil zeigen. Dies ist wertvoll, um zum Beispiel herauszufinden, ob ein Type bestandteil eines 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 dabei auf das eigentliche union Type-Objekt.

Bei zwischengespeicherten Type-Objekten wie oben exemplarisch aufgezeigt, sind die parent Eigenschaften nicht immer die echten Elternteile. So zum Beispiel bei einer Klasse, die mehrfach genutzt wird, zeigen zwar unmittelbaren Typen in types (TypePropertySignature und TypeMethodSignature) auf das korrekte TypeClass, aber die type dieser Signature-Typen zeigen auf die Signature-Typen des TypeClass des Cache-Eintrages. Das ist wichtig zu wissen, um so nicht unendlich die parent-Struktur auszulesen, sondern nur der unmittelbare Elternteil. Die Tatsache, dass der parent nicht unendliche Genauigkeit hat, ist Performance-Gründen geschuldet.

JIT Cache

Im weiteren Verlauf werden einige Funktionen und Features beschrieben, die oft auf die Type-Objekte basieren. Um einige davon performant umzusetzen, braucht es einen JIT-Cache (just in time) pro Type-Objekt. Die kann via getJitContainer(type) bereitgestellt werden. Diese Funktion gibt ein simples Objekt zurück, auf den beliebig Daten gespeichert werden können. Solange keine Referenz auf das Objekt gehalten wird, löscht es sich automatisch durch den GC sobald das Type-Objekt selbst auch nicht mehr referenziert wird.

Reflection-Klassen

Zusätzlich zu der typeOf<>() Funktion gibt es diverse Reflection-Klassen, die eine OOP-Alternative zu den Type-Objekten bieten. Die Reflection-Klassen sind nur für Klassen, Interface/Object-literale und Funktionen und deren direkte Unter-Typen (Properties, Methods, Parameter) vorhanden. Alle tieferen Typen müssen wieder mit den Type-Objekten ausgelesen 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

Typeninformation empfangen

Um selbst Funktionen bereitzustellen, die auf Typen operieren, kann es nützlich sein, dem User anzubieten, einen Typen manuell zu übergeben. Zum Beispiel könnte bei einer Validierungsfunktion es sinnvoll sein, als ersten Type-Argument den zu wünschenden Typen bereitzustellen und als erstes Funktionsargument die zu validierende Daten.

validate<string>(1234);

Damit diese Funktion den Typ string erhält, muss es dieses dem Typen-Compiler mitteilen.

function validate<T>(data: any, type?: ReceiveType<T>): void;

ReceiveType mit der Referenz auf den ersten Typenargumenten T signalisiert dem Typen-Compiler, dass jeder Aufruf zu validate den Type an zweiter Stelle (da type an zweiter Stelle deklariert ist) stellen soll. Um zur Laufzeit dann die Informationen auszulesen, wird die Funktion resolveReceiveType genutzt.

import { resolveReceiveType, ReceiveType } from '@deepkit/type';

function validate<T>(data: any, type?: ReceiveType<T>): void {
    type = resolveReceiveType(type);
}

Es ist nützlich, das Ergebnis derselben Variable zuzuweisen, um nicht unnötig eine neue anzulegen. In type ist nun entweder ein Typen-Objekt abgelegt oder es wird ein Fehler geworfen, wenn zum Beispiel kein Typen-Argument übergeben wurde, Deepkit’s Typen-Compiler nicht richtig installiert wurde, oder das Emitieren von Typeninformationen nicht aktiviert ist (siehe dazu die Sektion Installation weiter oben).

Bytecode

Um im Detail zu lernen, wie Deepkit die Typeninformationen im JavaScript enkodiert und ausliest, ist dieses Kapitel gedacht. Es erklärt, wie die Typen konkret in Bytecode umgewandelt, im JavaScript emittiert, und anschließen zur Laufzeit interpretiert werden.

Typen-Compiler

Der Type-Compiler (in @deepkit/type-compiler) ist dafür Verantwortlich, die definierten Typen in den TypeScript Dateien auszulesen und in ein Bytecode zu kompilieren. Dieser Bytecode hat alles, was nötig ist, um die Typen in der Laufzeit auszuführen. Zum Zeitpunkt dieses Buches ist der Type-Compiler ein sogenannter TypeScript Transformer. Dieser Transformer ist ein Plugin für den TypeScript Compiler selbst und wandelt ein TypeScript AST (Abstract Syntax Tree) in ein anderen TypeScript AST um. Deepkit’s Typen-Compiler liest in diesem Prozess den AST aus, produziert den dazugehörigen Bytecode, und fügt diesen in den AST ein.

TypeScript selbst erlaubt es nicht, dieses Plugin aka Transformer über eine tsconfig.json zu konfigurieren. Es ist entweder nötig, die TypeScript Compiler API direkt zu benutzen, oder ein Buildsystem wie Webpack mit ts-loader. Um diesen unangenehmen Weg den Benutzern von Deepkit zu ersparen, installiert sich der Deepkit Typen-Compiler automatisch selbst in node_modules/typescript sobald @deepkit/type-compiler installiert wird. Dies macht es möglich, dass alle Buildtools, die auf das lokal installierte TypeScript (das in node_modules/typescript) zugreifen, automatisch den Typen-Compiler aktiviert haben. Dadurch funktioniert tsc, Angular, webpack, ts-node, und einige andere Tools automatisch mit Deepkit’s Typen-Compiler.

Ist das automatische Ausführen von NPM install scripts nicht aktiviert und wird so das lokal installierte Typescript nicht modifiziert, so muss dieser Prozess manuell ausgeführt werden, sofern man das möchte. Alternative kann auch der Typen-Compiler manuell in einen Buildtool wie zum Beispiel webpack genutzt werden. Siehe dazu die Sektion Installation weiter oben.

Bytecode Encoding

Der Bytecode ist eine Folge von Befehlen für eine Virtuelle Maschine und ist im JavaScript selbst als Array mit Referenzen und String (dem eigentlichen Bytecode) enkodiert.

//TypeScript
type TypeA = string;

//generated JavaScript
const typeA = ['&'];

Die vorhandenen Befehle selbst sind jeweils ein Byte groß und in @deepkit/type-spec als ReflectionOp Enum zu finden. Zum Zeitpunkt dieses Buches ist der Befehlssatz über 81 Befehle gross.

enum ReflectionOp {
    never,
    any,
    unknown,
    void,
    object,

    string,
    number,

    //...many more
}

Eine Folge von Befehlen wird enkodiert als einen String um Speicherplatz zu sparen. So wird ein Typ string[] als Bytecode Program [string, array] konzeptionell dargestellt, welches die Bytes [5, 37] hat und mit folgendem Algorithmus enkodiert:

function encodeOps(ops: ReflectionOp[]): string {
    return ops.map(v => String.fromCharCode(v + 33)).join('');
}

Entsprechend wird aus einer 5 ein &-Zeichen und aus einer 37 ein F-Zeichen. Zusammen wird daraus &F und in Javascript als ['&F'] emittiert.

//TypeScript
export type TypeA = string[];

//generated JavaScript
export const __ΩtypeA = ['&F'];

Um Namenskonflikte vorzubeugen, erhält jeder Typ ein "__Ω" als Prefix. Für jeden explizit definierten Typen, der exportiert oder von einem exportierten Typen genutzt wird, wird ein Bytecode das JavaScript emittiert. Auch Klassen und Funktionen erhalten einen Bytecode direkt als Eigenschaft.

//TypeScript
function log(message: string): void {}

//generated JavaScript
function log(message) {}
log.__type = ['message', 'log', 'P&2!$/"'];

Virtuelle Maschine

Eine virtuelle Maschine (in @deepkit/type die Klasse Processor) zur Laufzeit ist dafür zuständig den encodierten Bytecode zu dekodieren und auszuführen. Sie gibt immer ein Typen-Objekt zurück, siehe weiter oben die Sektion Reflection.