Deepkit Runtime Types

Type Annotations

Type annotations are normal TypeScript types that contain meta-information that can be read and change the behavior of various functions at runtime. Deepkit already provides some type annotations that cover many 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 annotations 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 annotations, 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 annotations are used for example.

Following is a list of existing type annotations. The validator and serializer of @deepkit/type and @deepkit/bson and 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:

integerAn integer of arbitrary size.
int8An integer between -128 and 127.
uint8An integer between 0 and 255.
int16An integer between -32768 and 32767.
uint16An integer between 0 and 65535.
int32An integer between -2147483648 and 2147483647.
uint32An integer between 0 and 4294967295.
floatSame as number, but might have different meaning in database context.
float32A 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.
float64Same 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 that here, for example, no floats may be used in validation and the serializer automatically converts floats into 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 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;
}

PrimaryKey

Marks the field as primary key. Only used in the database context.

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

interface User {
    id: number & PrimaryKey;
}

AutoIncrement

Marks the field as auto increment. Only used in the database context. Usually together with PrimaryKey.

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

interface User {
    id: number & PrimaryKey & AutoIncrement;
}

Reference

Marks the field as reference (foreign key). Only used in the database context.

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

interface User {
    id: number & PrimaryKey;
    group: number & Reference<Group>;
}

interface Group {
    id: number & PrimaryKey;
}

In this example User.group is an owning reference also known as foreign key in SQL. This means that the User table has a column group that references the Group table. The Group table is the target table of the reference.

BackReference

Marks the field as back reference. Only used in the database context.

interface User {
    id: number & PrimaryKey;
    group: number & Reference<Group>;
}

interface Group {
    id: number & PrimaryKey;
    users: User[] & BackReference;
}

In this example Group.users is a back reference. This means that the User table has a column group that references the Group table. The Group has a virtual property users that is automatically populated with all users that have the same group id as the Group id once a database query with joins is executed. The property users is not stored in the database.

Index

Marks the field as index. Only used in the database context.

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

interface User {
    id: number & PrimaryKey;
    username: string & Index;
}

Unique

Marks the field as unique. Only used in the database context.

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

interface User {
    id: number & PrimaryKey;
    username: string & Unique;
}

DatabaseField

With DatabaseField you can define the database specific options like the real database column type, and the default value, etc.

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

interface User {
    id: number & PrimaryKey;
    username: string & DatabaseField<{ type: 'varchar(255)' }>;
}

Validation

TODO

See Validation Constraint Types.

InlineRuntimeType

To inline a runtime type. Only used in advanced cases.

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

const type: Type = { kind: ReflectionKind.string };

type Query = {
    field: InlineRuntimeType<typeof type>;
}

const resolved = typeOf<Query>(); // { field: string }

In TypeScript the type Query is { field: any }, but in runtime it's { field: string }.

This is useful if you build a highly customizable system where you accept runtime types, and you reuse them in various other cases.

ResetAnnotation

To reset all annotations of a property. Only used in advanced cases.

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

interface User {
    id: number & PrimaryKey;
}

interface UserCreationPayload {
    id: User['id'] & ResetAnnotation<'primaryKey'>;
}

Custom Type Annotations

You can define your own type annotations.

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

By convention, a type annotation 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 annotation to be equipped with additional options.

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

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

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

The type annotations 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 annotation myAnnotation was used or undefined if not. If the type annotation has additional options as seen in AnnotationOption, the passed values can be found in the array. Already supplied type annotations 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.