Runtime Types

Making type information available at runtime in TypeScript changes a lot. It allows new ways of working that were previously only possible in a roundabout way, or not at all. Declaring types and schemas has become a big part of modern development processes. For example, GraphQL, validators, ORMs, and encoders such as ProtoBuf, and many more rely on having schema information available at runtime to provide fundamental functionality in the first place. These tools and libraries sometimes require the developer to learn completely new languages that have been developed very specifically for the use case. For example, ProtoBuf and GraphQL have their own declaration language, also validators are often based on their own schema APIs or even JSON schema, which is also an independent way to define structures. Some of them require code generators to be executed whenever a change is made, in order to provide the schema information to the runtime as well. Another well-known pattern is to use experimental TypeScript decorators to provide meta-information to classes at runtime.

But is all this necessary? TypeScript offers a very powerful language to describe even very complex structures. In fact, TypeScript is now touring-complete, which roughly means that theoretically any kind of program can be mapped into TypeScript. Of course, this has its practical limitations, but the important point is that TypeScript is able to completely replace any declaration formats such as GraphQL, ProtoBuf, JSON Schema, and many others. Combined with a type system at runtime, it is possible to cover all the described tools and their use cases in TypeScript itself without any code generator. But why is there not yet a solution that allows exactly this?

Historically, TypeScript has undergone a massive transformation over the past few years. It has been completely rewritten several times, received basic features, and undergone a number of iterations and breaking changes. However, TypeScript has now reached a product market fit that greatly slows the rate at which fundamental innovations and breaking changes happen. TypeScript has proven itself and shown what a highly charming type system for a highly dynamic language like JavaScript should look like. The market has gratefully embraced this push and ushered in a new era in JavaScript development.

This is exactly the time to build tools on top of the language itself at a fundamental level to make the above possible. Deepkit wants to be the impetus to bring over decades of proven design patterns from the enterprise of languages like Java and PHP not only fundamental to TypeScript, but in a new and better way that works with JavaScript rather than against it. Through type information at runtime, these are now for the first time not only possible in principle, but allow for whole new much simpler design patterns that are not possible with languages like Java and PHP. TypeScript itself has laid the foundation here to make the developer’s life considerably easier with completely new approaches in strong combination with the tried and tested.

Reading type information at runtime is the capability on which Deepkit builds its foundation. The API of the Deepkit libraries are largely focused on using as much TypeScript type information as possible to be as efficient as possible. Type system at runtime means that type information is readable at runtime and dynamic types are computable. This means, for example, that for classes all properties and for functions all parameters and return types can be read.

Let’s take this function as an example:

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

In JavaScript itself, several pieces of information can be read at runtime. For example, the name of the function (unless modified with a minimizer):

log.name; //‘log’

On the other hand, the number of parameters can be read out:

log.length; //1

With a bit more code it is also possible to read out the names of the parameters. However, this is not easily done without a rudimentary JavaScript parser or RegExp on log.toString(), so that’s about it from here. Since TypeScript translates the above function into JavaScript as follows:

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

the information that message is of type string and the return type is of type void is no longer available. This information has been irrevocably destroyed by TypeScript.

However, with a type system at runtime, this information can survive so that one can programmatically read the types of message and the return type.

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 does just that. It hooks into the compilation of TypeScript and ensures that all type information is built into the generated JavaScript. Functions like typeOf() (not to be confused with the operator typeof, with a lowercase o) then allow the developer to access it. Libraries can therefore be developed based on this type information, allowing the developer to use already written TypeScript types for a whole range of application possibilities.

Installation

To install Deepkit’s runtime type system two packages are needed. The type compiler in @deepkit/type-compiler and the runtime in @deepkit/type. The type compiler can be installed in package.json devDependencies, because it is only needed at build time.

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

Runtime type information is not generated by default. It must be set "reflection": true in the tsconfig.json file to enable it in all files in the same folder of this file or in all subfolders. If decorators are to be used, "experimentalDecorators": true must be enabled in tsconfig.json. This is not strictly necessary to work with @deepkit/type, but necessary for certain functions of other deepkit libraries and in @deepkit/framework.

file: tsconfig.json

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

Type Compiler

TypeScript itself does not allow to configure the type compiler via a tsconfig.json. It is necessary to either use the TypeScript compiler API directly or a build system like Webpack with ts-loader. To avoid this inconvenience for Deepkit users, the Deepkit type compiler automatically installs itself in node_modules/typescript when @deepkit/type-compiler is installed (this is done via NPM install hooks). This makes it possible for all build tools that access the locally installed TypeScript (the one in node_modules/typescript) to automatically have the type compiler enabled. This makes tsc, Angular, webpack, ts-node, and some other tools automatically work with the Deepkit type compiler.

If the type compiler could not be successfully installed automatically (for example because NPM install hooks are disabled), this can be done manually with the following command:

node_modules/.bin/deepkit-type-install

Note that deepkit-type-install must be run if the local typescript version has been updated (for example, if the typescript version in package.json has changed and npm install is run).

Webpack

If you want to use the type compiler in a webpack build, you can do so with the ts-loader package (or any other typescript loader that supports transformer registration).

file: 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/,
       },
    ],
  },
}

Type Decorators

Type decorators are normal TypeScript types that contain meta-information to change the behavior of various functions at runtime. Deepkit already provides some type decorators that cover some use cases. For example, a class property can be marked as primary key, reference, or index. The database library can use this information at runtime to create the correct SQL queries without prior code generation. Validator constraints such as MaxLength, Maximum, or Positive can also be added to any type. It is also possible to tell the serializer how to serialize or deserialize a particular value. In addition, it is possible to create completely custom type decorators and read them at runtime, in order to use the type system at runtime in a very individual way.

Deepkit comes with a whole set of type decorators, all of which can be used directly from @deepkit/type. They are designed not to come from multiple libraries, so as not to tie code directly to a particular library such as Deepkit RPC or Deepkit Database. This allows easier reuse of types, even in the frontend, although database type decorators are used for example.

The following is a list of existing type decorators. The validator and serializer of @deepkit/type and @deepkit/bson as well as Deepkit Database of @deepkit/orm used this information differently. See the corresponding chapters to learn more about this.

Integer/Float

Integer and floats are defined as a base as number and has several sub-variants:

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;
}

Here the id of the user is a number at runtime, but is interpreted as an integer in the validation and serialization. This means, for example, that floats may not be used in validation and the serializer automatically converts floats to integers.

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

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

The subtypes can be used in the same way and are useful if a specific range of numbers is to be allowed.

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 is usually stored as a binary in the database and as a string in JSON.

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 a 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

Custom Type Decorators

A type decorator can be defined as follows:

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

By convention, a type decorator is defined to be an object literal with a single optional property __meta that has a tuple as its type. The first entry in this tuple is its unique name and all subsequent tuple entries are arbitrary options. This allows a type decorator to be equipped with additional options.

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

The type decorator is used with the intersection operator &. Any number of type decorators can be used on one type.

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

The type decorators can be read out via the type objects of typeOf<T>() and metaAnnotation:

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

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

The result in annotation is either an array with options if the type decorator myAnnotation was used or undefined if not. If the type decorator has additional options as seen in AnnotationOption, the passed values can be found in the array. Already supplied type decorators like MapName, Group, Data, etc have their own annotation object:

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

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

See Runtime Types Reflection to learn more.

External Classes

Since TypeScript does not include type information by 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

To work directly with the type information itself, there are two basic variants: Type objects and Reflection classes. Reflection classes are discussed below. The function typeOf returns type objects, which are very simple object literals. It always contains a kind which is a number and gets its meaning from the enum ReflectionKind. ReflectionKind is defined in the @deepkit/type package as follows:

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
}

There are a number of possible type objects that can be returned. The simplest ones are never, any, unknown, void, null, and undefined, which are represented as follows:

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

For example, number 0 is the first entry of the ReflectionKind enum, in this case never, number 1 is the second entry, here any, and so on. Accordingly, primitive types like string, number, boolean are represented as follows:

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

These rather simple types have no further information at the type object, because they were passed directly as type argument to typeOf. However, if types are passed via type aliases, additional information can be found at the type object.

type Title = string;

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

In this case, the name of the type alias 'Title' is also available. If a type alias is a generic, the types passed will also be available at the type object.

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

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

If the type passed is the result of an index access operator, the container and the index type are present:

interface User {
  id: number;
  username: string;
}

typeOf<User['username']>();
{kind: 5, indexAccessOrigin: {
    container: {kind: Reflection.objectLiteral, types: [...]},
    Index: {kind: Reflection.literal, literal: 'username'}
}}

Interfaces and object literals are both output as Reflection.objectLiteral and contain the properties and methods in the 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 are also in the 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

Classes are similar to object literals and also have their properties and methods under a types array in addition to classType which is a reference to the class itself.

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}},
  ]
}

Note that the type of Reflection.propertySignature has changed to Reflection.property and Reflection.methodSignature has changed to Reflection.method. Since properties and methods on classes have additional attributes, this information can also be retrieved. The latter additionally include visibility, abstract, and default. Type objects of classes contain only the properties and methods of the class itself and not of the super-classes. This is contrary to type objects of interfaces/object-literals, which have all property signatures and method signatures of all parents resolved into types. To resolve the property and method of the super-classes, either ReflectionClass and its ReflectionClass.getProperties() (see following sections) or resolveTypeMembers() of @deepkit/type can be used.

There is a whole plethora of type objects. For example for literal, template literals, promise, enum, union, array, tuple, and many more. To find out which ones all exist and what information is available, it is recommended to import type from @deepkit/type. It is a union with all possible subtypes like TypeAny, TypeUnknonwn, TypeVoid, TypeString, TypeNumber, TypeObjectLiteral, TypeArray, TypeClass, and many more. There you can find the exact structure.

Type Cache

Type objects are cached for type aliases, functions, and classes as soon as no generic argument is passed. This means that a call to typeOf<MyClass>() always returns the same object.

type MyType = string;

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

However, as soon as a generic type is used, new objects are always created, even if the type passed is always the same. This is because an infinite number of combinations are theoretically possible and such a cache would effectively be a memory leak.

type MyType<T> = T;

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

However, as soon as a type is instantiated multiple times in a recursive type, it is cached. However, the duration of the cache is limited only to the moment the type is computed and does not exist thereafter. Also, although the Type object is cached, a new reference is returned and is not the exact same object.

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

typeOf<Object>();

MyType<string> is cached as long as Object is computed. The PropertySignature of a and b thus have the same type from the cache, but are not the same Type object.

All non-root type objects have a parent property, which usually points to the enclosing parent. This is valuable, for example, to find out whether a Type is part of a union or not.

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' points to the actual union type object.

For cached type objects as exemplified above, the parent properties are not always the real parents. For example, for a class that is used multiple times, although immediate types in types (TypePropertySignature and TypeMethodSignature) point to the correct TypeClass, the type of these signature types point to the signature types of the TypeClass of the cached entry. This is important to know so as not to infinitely read the parent structure, but only the immediate parent. The fact that the parent does not have infinite precision is due to performance reasons.

JIT Cache

In the further course some functions and features are described, which are often based on the type objects. To implement some of them in a performant way, a JIT (just in time) cache per type object is needed. This can be provided via getJitContainer(type). This function returns a simple object on which arbitrary data can be stored. As long as no reference to the object is held, it will be deleted automatically by the GC as soon as the Type object itself is also no longer referenced.

Reflection Classes

In addition to the typeOf<>() function, there are various reflection classes that provide an OOP alternative to the Type objects. The reflection classes are only available for classes, interface/object literals and functions and their direct sub-types (properties, methods, parameters). All deeper types must be read again with the Type objects.

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

Receive Type Information

In order to provide functions that operate on types, it can be useful to offer the user to pass a type manually. For example, in a validation function, it might be useful to provide the type to be requested as the first type argument and the data to be validated as the first function argument.

validate<string>(1234);

In order for this function to receive the type string, it must communicate this to the type compiler.

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

ReceiveType with the reference to the first type arguments T signals the type compiler that each call to validate should put the type in second place (since type is declared in second place). To then read out the information at runtime, the resolveReceiveType function is used.

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

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

It is useful to assign the result to the same variable to avoid creating a new one unnecessarily. In type now either a type object is stored or an error is thrown, if for example no type argument was passed, Deepkit’s type compiler was not installed correctly, or the emitting of type information is not activated (see the section Installation above).

Bytecode

To learn in detail how Deepkit encodes and reads the type information in JavaScript, this chapter is intended. It explains how the types are actually converted into bytecode, emitted in JavaScript, and then interpreted at runtime.

Type Compiler

The type compiler (in @deepkit/type-compiler) is responsible for reading the defined types in the TypeScript files and compiling them into a bytecode. This bytecode has everything needed to execute the types in runtime. At the time of this writing, the Type compiler is a so-called TypeScript Transformer. This transformer is a plugin for the TypeScript compiler itself and converts a TypeScript AST (Abstract Syntax Tree) into another TypeScript AST. In this process, Deepkit’s type compiler reads the AST, produces the corresponding bytecode, and inserts it into the AST.

TypeScript itself does not allow to configure this plugin aka transformer via a tsconfig.json. It is either necessary to use the TypeScript compiler API directly, or a build system like Webpack with ts-loader. To avoid this inconvenience for Deepkit users, the Deepkit type compiler automatically installs itself in node_modules/typescript when @deepkit/type-compiler is installed. This makes it possible for all build tools that access the locally installed TypeScript (the one in node_modules/typescript) to automatically have the type compiler enabled. This makes tsc, Angular, webpack, ts-node, and some other tools work automatically with Deepkit’s type compiler.

If the automatic execution of NPM install scripts is not activated and thus the locally installed typescript is not modified, this process must be executed manually if you want to do so. Alternatively, the types compiler can be used manually in a build tool such as webpack. See the Installation section above.

Bytecode Encoding

The bytecode is a sequence of commands for a virtual machine and is encoded in the JavaScript itself as an array of references and string (the actual bytecode).

//TypeScript
type TypeA = string;

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

The existing commands themselves are each one byte in size and can be found in @deepkit/type-spec as ReflectionOp enums. At the time of this writing, the command set is over 81 commands in size.

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

    string,
    number,

    //...many more
}

A sequence of commands is encoded as a string to save memory. So a type string[] is conceptualized as a bytecode program [string, array] which has the bytes [5, 37] and encoded with the following algorithm:

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

Accordingly, a 5 becomes an & character and a 37 becomes an F character. Together they become &F and are emitted in Javascript as ['&F'].

//TypeScript
export type TypeA = string[];

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

To prevent naming conflicts, each type is given a "_Ω" prefix. For each explicitly defined type that is exported or used by an exported type, a bytecode is emitted the JavaScript. Classes and functions also receive a bytecode directly as a property.

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

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

Virtual Machine

A virtual machine (in @deepkit/type the class Processor) at runtime is responsible for decoding and executing the encoded bytecode. It always returns a type object, see the Reflection section above.