Typannotationen
Typannotationen sind normale TypeScript-Types, die Metainformationen enthalten, die zur Laufzeit ausgelesen werden können und das Verhalten verschiedener Functions zur Laufzeit ändern. Deepkit stellt bereits einige Typannotationen bereit, die viele Anwendungsfälle abdecken. Zum Beispiel kann eine Klassen-Property als Primary Key, Reference oder Index markiert werden. Die Datenbankbibliothek kann diese Informationen zur Laufzeit nutzen, um ohne vorherige Code-Generierung die richtigen SQL-Abfragen zu erstellen.
Validator-Constraints wie MaxLength
, Maximum
oder Positive
können ebenfalls zu jedem Type hinzugefügt werden. Es ist außerdem möglich, dem Serializer mitzuteilen, wie ein bestimmter Wert serialisiert oder deserialisiert werden soll. Darüber hinaus ist es möglich, vollständig eigene Typannotationen zu erstellen und diese zur Laufzeit auszulesen, um das Type-System zur Laufzeit sehr individuell zu nutzen.
Deepkit bringt einen ganzen Satz an Typannotationen mit, die alle direkt aus @deepkit/type
genutzt werden können. Sie sind so konzipiert, dass sie nicht aus mehreren Libraries stammen, um Code nicht direkt an eine bestimmte Library wie Deepkit RPC oder Deepkit Database zu binden. Dadurch lassen sich Types leichter wiederverwenden, sogar im Frontend, auch wenn zum Beispiel Datenbank-Typannotationen verwendet werden.
Im Folgenden befindet sich eine Liste vorhandener Typannotationen. Der Validator und Serializer von @deepkit/type
und @deepkit/bson
sowie die Deepkit Database von @deepkit/orm
nutzen diese Informationen jeweils unterschiedlich. Siehe die entsprechenden Kapitel, um mehr darüber zu erfahren.
Integer/Float
Integer und Floats sind als Basis als number
definiert und haben mehrere Untervarianten:
integer | Ein Integer beliebiger Größe. |
int8 | Ein Integer zwischen -128 und 127. |
uint8 | Ein Integer zwischen 0 und 255. |
int16 | Ein Integer zwischen -32768 und 32767. |
uint16 | Ein Integer zwischen 0 und 65535. |
int32 | Ein Integer zwischen -2147483648 und 2147483647. |
uint32 | Ein Integer zwischen 0 und 4294967295. |
float | Entspricht number, kann im Datenbankkontext jedoch eine andere Bedeutung haben. |
float32 | Ein Float zwischen -3.40282347e+38 und 3.40282347e+38. Beachte, dass JavaScript den Bereich aufgrund von Präzisionsproblemen nicht korrekt prüfen kann; die Information kann jedoch für die Datenbank oder binäre Serializer nützlich sein. |
float64 | Entspricht number, kann im Datenbankkontext jedoch eine andere Bedeutung haben. |
import { integer } from '@deepkit/type'; interface User { id: integer; }
Hier ist die id
des Users zur Laufzeit eine number, wird aber in Validierung und Serialisierung als Integer interpretiert.
Das bedeutet, dass beispielsweise in der Validierung keine Floats erlaubt sind und der Serializer Floats automatisch in Integer umwandelt.
import { is, integer } from '@deepkit/type'; is<integer>(12); //true is<integer>(12.5); //false
Die Subtypes können auf die gleiche Weise verwendet werden und sind nützlich, wenn ein bestimmter Zahlenbereich 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
import { is, float, float32, float64 } from '@deepkit/type'; is<float>(12.5); //true is<float32>(12.5); //true is<float64>(12.5); //true
UUID
UUID v4 wird in der Datenbank üblicherweise als Binary und in JSON als string gespeichert.
import { is, UUID } from '@deepkit/type'; is<UUID>('f897399a-9f23-49ac-827d-c16f8e4810a0'); //true is<UUID>('asd'); //false
MongoID
Markiert dieses Feld als ObjectId für MongoDB. Wird als string aufgelöst. Wird in MongoDB als Binary gespeichert.
import { MongoId, serialize, is } from '@deepkit/type'; serialize<MongoId>('507f1f77bcf86cd799439011'); //507f1f77bcf86cd799439011 is<MongoId>('507f1f77bcf86cd799439011'); //true is<MongoId>('507f1f77bcf86cd799439011'); //false class User { id: MongoId = ''; //wird in Deepkit ORM automatisch gesetzt, sobald der User eingefügt wurde }
Bigint
Standardmäßig serialisiert der normale bigint Type als number in JSON (und als long in BSON). Dies hat jedoch Einschränkungen hinsichtlich dessen, was gespeichert werden kann, da bigint in JavaScript eine unbegrenzte potenzielle Größe hat, während numbers in JavaScript und long in BSON begrenzt sind. Um diese Einschränkung zu umgehen, stehen die Types BinaryBigInt
und SignedBinaryBigInt
zur Verfügung.
BinaryBigInt
ist dasselbe wie bigint, serialisiert in Datenbanken jedoch als unsigniertes Binary mit unbegrenzter Größe (anstatt 8 Bytes in den meisten Datenbanken) und in JSON als string. Negative Werte werden in positive umgewandelt (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 speichert BinaryBigInt als Binärfeld.
SignedBinaryBigInt
ist dasselbe wie BinaryBigInt
, kann jedoch auch negative Werte speichern. Deepkit ORM speichert SignedBinaryBigInt
als Binary. Das Binary hat ein zusätzliches führendes Vorzeichen-Byte und wird als uint dargestellt: 255 für negativ, 0 für null oder 1 für positiv.
import { SignedBinaryBigInt } from '@deepkit/type'; interface User { id: SignedBinaryBigInt; }
MapName
Um den Namen einer Property in der Serialisierung zu ändern.
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 können gruppiert werden. Bei der Serialisierung kann man zum Beispiel eine Group von der Serialisierung ausschließen. Siehe das Kapitel Serialisierung für weitere Informationen.
import { serialize } from '@deepkit/type'; interface Model { username: string; password: string & Group<'secret'> } serialize<Model>( { username: 'Peter', password: 'nope' }, { groupsExclude: ['secret'] } ); //{username: 'Peter'}
Data
Jede Property kann zusätzliche Metadaten hinzufügen, die über die Reflection API ausgelesen werden können. Siehe Laufzeit-Typen-Reflexion für weitere Informationen.
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
Jede Property kann für ein bestimmtes Ziel vom Serialisierungsprozess ausgeschlossen werden.
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, da der Standard-Serializer von deserialize `json` heißt item.password = 'secret'; const json = serialize<Auth>(item); json.password; //wieder undefined, da der Serializer von serialize `json` heißt
Embedded
Markiert das Feld als 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' } //für deserialize muss die eingebettete Struktur bereitgestellt werden deserialize<User>({ id: 12, address_street: 'abc', //... });
Es ist möglich, das Präfix zu ändern (standardmäßig ist es der Property-Name).
interface User { id: number & PrimaryKey; address: Embedded<Address, { prefix: 'addr_' }>; } serialize<User>(user); { id: 12, addr_street : 'abc', addr_postalCode : '1234', } //oder vollständig entfernen interface User { id: number & PrimaryKey; address: Embedded<Address, { prefix: '' }>; } serialize<User>(user); { id: 12, street : 'abc', postalCode : '1234', }
Entity
Um Interfaces mit Entity-Informationen zu annotieren. Wird nur im Datenbankkontext verwendet.
import { Entity, PrimaryKey } from '@deepkit/type'; interface User extends Entity<{ name: 'user', collection: 'users'> { id: number & PrimaryKey; username: string; }
PrimaryKey
Markiert das Feld als Primary Key. Wird nur im Datenbankkontext verwendet.
import { PrimaryKey } from '@deepkit/type'; interface User { id: number & PrimaryKey; }
AutoIncrement
Markiert das Feld als Auto-Increment. Wird nur im Datenbankkontext verwendet.
Üblicherweise zusammen mit PrimaryKey
.
import { AutoIncrement } from '@deepkit/type'; interface User { id: number & PrimaryKey & AutoIncrement; }
Reference
Markiert das Feld als Reference (Foreign Key). Wird nur im Datenbankkontext verwendet.
import { Reference } from '@deepkit/type'; interface User { id: number & PrimaryKey; group: number & Reference<Group>; } interface Group { id: number & PrimaryKey; }
In diesem Beispiel ist User.group
eine besitzende Reference, auch bekannt als Foreign Key in SQL. Das bedeutet, dass die User
-Tabelle eine Spalte group
hat, die auf die Group
-Tabelle verweist. Die Group
-Tabelle ist die Ziel-Tabelle der Reference.
BackReference
Markiert das Feld als Back Reference. Wird nur im Datenbankkontext verwendet.
interface User { id: number & PrimaryKey; group: number & Reference<Group>; } interface Group { id: number & PrimaryKey; users: User[] & BackReference; }
In diesem Beispiel ist Group.users
eine Back Reference. Das bedeutet, dass die User
-Tabelle eine Spalte group
hat, die auf die Group
-Tabelle verweist.
Die Group
hat eine virtuelle Property users
, die automatisch mit allen Benutzern befüllt wird, die dieselbe group
-ID wie die Group
-ID haben, sobald eine Datenbankabfrage
mit Joins ausgeführt wird. Die Property users
wird nicht in der Datenbank gespeichert.
Index
Markiert das Feld als Index. Wird nur im Datenbankkontext verwendet.
import { Index } from '@deepkit/type'; interface User { id: number & PrimaryKey; username: string & Index; }
Unique
Markiert das Feld als Unique. Wird nur im Datenbankkontext verwendet.
import { Unique } from '@deepkit/type'; interface User { id: number & PrimaryKey; username: string & Unique; }
DatabaseField
Mit DatabaseField
können datenbankspezifische Optionen wie der konkrete Datenbank-Spaltentyp und der Default-Wert usw. definiert werden.
import { DatabaseField } from '@deepkit/type'; interface User { id: number & PrimaryKey; username: string & DatabaseField<{ type: 'varchar(255)' }>; }
Validation
TODO
Siehe Validierungs-Constraint-Typen.
InlineRuntimeType
Um einen Runtime Type inline einzubetten. Wird nur in fortgeschrittenen Fällen verwendet.
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 ist der Type Query
{ field: any }
, zur Laufzeit jedoch { field: string }
.
Dies ist nützlich, wenn du ein hochgradig anpassbares System baust, in dem du Runtime Types akzeptierst und sie in verschiedenen anderen Fällen wiederverwendest.
ResetAnnotation
Um alle Annotations einer Property zurückzusetzen. Wird nur in fortgeschrittenen Fällen verwendet.
import { ResetAnnotation } from '@deepkit/type'; interface User { id: number & PrimaryKey; } interface UserCreationPayload { id: User['id'] & ResetAnnotation<'primaryKey'>; }
Custom Type Annotations
Du kannst eigene Typannotationen definieren.
type MyAnnotation = { __meta?: ['myAnnotation'] };
Konventionsgemäß wird eine Typannotation als Objektliteral mit einer einzelnen optionalen Property __meta
definiert, die ein Tupel als Type hat. Der erste Eintrag in diesem Tupel ist sein eindeutiger Name und alle nachfolgenden Tupel-Einträge sind beliebige Optionen. Dadurch kann eine Typannotation mit zusätzlichen Optionen ausgestattet werden.
type AnnotationOption<T extends { title: string }> = { __meta?: ['myAnnotation', T] };
Die Typannotation wird mit dem Schnittmengen-Operator &
verwendet. Es können beliebig viele Typannotationen auf einem Type genutzt werden.
type Username = string & MyAnnotation; type Title = string & MyAnnotation & AnnotationOption<{ title: 'Hello' }>;
Die Typannotationen können über die Type-Objekte von typeOf<T>()
und typeAnnotation
ausgelesen werden:
import { typeOf, typeAnnotation } from '@deepkit/type'; const type = typeOf<Username>(); const annotation = typeAnnotation.getForName(type, 'myAnnotation'); //[]
Das Ergebnis in annotation
ist entweder ein Array mit Optionen, falls die Typannotation myAnnotation
verwendet wurde, oder undefined
, falls nicht. Wenn die Typannotation zusätzliche Optionen hat, wie in AnnotationOption
zu sehen, finden sich die übergebenen Werte im Array.
Bereits mitgelieferte Typannotationen wie MapName
, Group
, Data
usw. haben ihr eigenes 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 Laufzeit-Typen-Reflexion, um mehr zu erfahren.