TypeScript für die Backend-Entwicklung

Die Java-Sprache ist in der Backend-Entwicklung nach wie vor oberstes Gebot. Dafür gibt es viele Gründe: Geschwindigkeit, Sicherheit (wenn Sie natürlich die Augen vor Nullzeigern verschließen) sowie ein riesiges, gut getestetes Ökosystem. Im Zeitalter von Microservices und agiler Entwicklung sind jedoch andere Faktoren wichtiger geworden. In einigen Systemen ist es möglicherweise nicht erforderlich, die Spitzenleistung aufrechtzuerhalten und über ein robustes Ökosystem stabiler Abhängigkeiten zu verfügen, wenn es um einen einfachen Dienst geht, der CRUD-Operationen und Datentransformationen ausführt. Darüber hinaus müssen viele Systeme schnell aufgebaut und neu aufgebaut werden, um mit der schnellen iterativen Entwicklung von Funktionen Schritt zu halten.



Dank der allgegenwärtigen Magie von Spring Boot ist es einfach, einen einfachen Java-Dienst zu entwickeln und bereitzustellen. Da jedoch geschlossene Klassen getestet und Daten transformiert werden müssen, sind in Ihrem Code zahlreiche Builder, Konverter, Aufzählungskonstruktoren und Serialisierer vorhanden, die den Weg für die Hölle des stereotypen Java-Codes ebnen. Aus diesem Grund verzögert sich die Entwicklung neuer Funktionen häufig. Und ja, die Codegenerierung funktioniert, ist aber nicht sehr flexibel.



TypeScript hat sich unter Backend-Entwicklern noch nicht gut etabliert. Wahrscheinlich, weil es sich um eine Reihe deklarativer Dateien handelt, mit denen Sie JavaScript einige Eingaben hinzufügen können. Trotzdem gibt es eine Menge Logik, für deren Darstellung Dutzende von Java-Zeilen erforderlich sind und die in nur wenigen Zeilen TypeScript dargestellt werden können.

Viele der Funktionen, die als typische Funktionen von TypeScript gelten, beziehen sich tatsächlich auf JavaScript. TypeScript kann jedoch auch als eigene Sprache angesehen werden, mit einigen syntaktischen und konzeptionellen Ähnlichkeiten zu JavaScript. Lassen Sie uns also für einen Moment von JavaScript abschweifen und TypeScript selbst betrachten: Es ist eine wunderschöne Sprache mit einem extrem leistungsfähigen und dennoch flexiblen Typensystem, tonnenweise syntaktischem Zucker und schließlich null Sicherheit!



Wir haben ein Repository auf Github mit einer benutzerdefinierten Node / TypeScript-Webanwendung zusammen mit einigen zusätzlichen Erklärungen gehostet . Es gibt auch einen fortgeschrittenen Zweig mit einem Beispiel für Zwiebelarchitektur und mehr nicht triviale Schreibkonzepte.



Einführung in TypeScript



Beginnen wir mit den Grundlagen: TypeScript ist eine asynchrone funktionale Programmiersprache, die dennoch Klassen und Schnittstellen sowie öffentliche, private und geschützte Attribute unterstützt. Wenn ein Programmierer mit dieser Sprache arbeitet, gewinnt er daher beträchtliche Flexibilität bei der Arbeit auf der Ebene der Mikroarchitektur und des Codestils. Der TypeScript-Compiler kann dynamisch konfiguriert werden, dh steuern, welche Importtypen zulässig sind, ob Funktionen explizite Rückgabetypen erfordern und ob bei der Kompilierung keine Nullprüfungen aktiviert sind.



Da TypeScript zu regulärem JavaScript kompiliert wird, wird Node.js als Backend-Laufzeit verwendet. Ohne ein umfassendes Framework, das Spring ähnelt, würde ein typischer Webdienst ein flexibleres Framework verwenden, das als Webserver dient ( Express.js ist ein gutes Beispiel dafür ). Folglich wird es sich als weniger "magisch" herausstellen und seine grundlegende Einrichtung und Konfiguration wird expliziter angeordnet. In diesem Fall erfordern relativ komplexe Dienste auch mehr Bastelarbeiten an der Konfiguration. Andererseits ist das Einrichten relativ kleiner Anwendungen nicht schwierig, und darüber hinaus ist es fast möglich, ohne vorher das Framework zu lernen.



Das Abhängigkeitsmanagement ist mit dem flexiblen und dennoch leistungsstarken Paketmanager npm von Node einfach.



Die Grundlagen



Wenn Klassen definieren public, sind Modifikatoren Zugriffskontrolle unterstützt , protectedund private, na ja die meisten Entwickler bekannt:



class Order {

    private status: OrderStatus;

    constructor(public readonly id: string, isSpecialOrder: boolean) {
        [...]
    }
}


Die Klasse hat jetzt Orderzwei Attribute: ein privates statusund ein idschreibgeschütztes öffentliches Feld . In Typoskript, Konstruktorargumente mit Schlüsselwörter public, protectedoder privateautomatisch werden Klassenattribute.



interface User {
    id?: string;
    name: string;
    t_registered: Date;
}

const user: User = { name: 'Bob', t_registered: new Date() };


Da TypeScript Typinferenz verwendet, kann das Benutzerobjekt auch dann instanziiert werden, wenn die Klasse Userselbst nicht bereitgestellt wird . Dieser strukturähnliche Ansatz wird häufig bei der Arbeit mit reinen Datenentitäten gewählt und erfordert keine Methoden oder internen Status.



Generika werden in TypeScript ähnlich wie in Java ausgedrückt:



class Repository<T extends StoredEntity> {
    findOneById(id: string): T {
        [...]
    }
}


Leistungsstarkes Typensystem



Das Herzstück des leistungsstarken Typensystems von TypeScript ist die Typinferenz. Es unterstützt auch die statische Eingabe. Statische Typanmerkungen sind jedoch optional, wenn der Rückgabetyp oder Parametertyp aus dem Kontext abgeleitet werden kann.



TypeScript ermöglicht auch die Verwendung von Vereinigungstypen, Teiltypen und Typschnittpunkten, wodurch die Sprache eine beträchtliche Flexibilität erhält und unnötige Komplexität vermieden wird. In TypeScript können Sie auch einen bestimmten Wert als Typ verwenden, was in einer Vielzahl von Situationen unglaublich nützlich ist.



Aufzählungen, Typinferenz und Unionstypen



Stellen Sie sich eine häufige Situation vor, in der der Auftragsstatus eine typsichere Darstellung (als Aufzählung) haben muss, für die JSON-Serialisierung jedoch auch eine Zeichenfolgendarstellung erforderlich ist. In Java wäre dies eine Aufzählung zusammen mit einem Konstruktor und einem Getter für Zeichenfolgenwerte.



Im ersten Beispiel können Sie mit TypeScript-Aufzählungen direkt eine Zeichenfolgendarstellung hinzufügen. Dadurch erhalten wir eine typsichere Aufzählungsdarstellung, die die zugehörige Zeichenfolgendarstellung automatisch serialisiert.



enum Status {
    ORDER_RECEIVED = 'order_received',
    PAYMENT_RECEIVED = 'payment_received',
    DELIVERED = 'delivered',
}

interface Order {
    status: Status;
}

const order: Order = { status: Status.ORDER_RECEIVED };


Beachten Sie die letzte Codezeile, in der durch Typinferenz ein Objekt instanziiert werden kann, das der Schnittstelle entspricht `Order`. Da wir keinen internen Zustand oder keine interne Logik in unsere Reihenfolge aufnehmen müssen, können wir auf Klassen und Konstruktoren verzichten.



Es stellt sich heraus, dass diese Aufgabe noch einfacher gelöst werden kann, wenn die Folgerung von Typen und Vereinigungstypen miteinander geteilt wird:



interface Order {
    status: 'order_received' | 'payment_received' | 'delivered';
}

const orderA: Order = { status: 'order_received' }; // 
const orderB: Order = { status: 'new' }; //  


Der TypeScript-Compiler akzeptiert nur die angegebene Zeichenfolge als gültigen Bestellstatus (beachten Sie, dass hierfür weiterhin die eingehenden JSON-Daten überprüft werden müssen).



Grundsätzlich funktionieren solche Typdarstellungen mit allem. Ein Typ kann durchaus eine Vereinigung eines Zeichenfolgenliteral, einer Zahl und eines anderen benutzerdefinierten Typs oder einer anderen benutzerdefinierten Schnittstelle sein. Weitere interessante Beispiele finden Sie im Advanced Typing Guide von TypeScript .



Lambdas und funktionale Argumente



Da TypeScript eine funktionale Programmiersprache ist, werden im Kern anonyme Funktionen, auch Lambdas genannt, unterstützt.



const evenNumbers = [ 1, 2, 3, 4, 5, 6 ].filter(i => i % 2 == 0);


Das obige Beispiel .filter()hat eine Funktion vom Typ (a: T) => boolean. Diese Funktion wird durch ein anonymes Lambda dargestellt i => i % 2 == 0. Im Gegensatz zu Java, wo Funktionsparameter einen expliziten Typ und eine Funktionsschnittstelle haben müssen, kann der Lambda-Typ auch anonym dargestellt werden:



class OrderService {
    constructor(callback: (order: Order) => void) {
        [...]
    }
}


Asynchrone Programmierung



Da TypeScript mit allen Einschränkungen eine Obermenge von JavaScript darstellt, ist die asynchrone Programmierung ein Schlüsselkonzept in dieser Sprache. Ja, Sie können hier Lambdas und Rückrufe verwenden. TypeScript verfügt über zwei wesentliche Mechanismen, mit denen Sie die Hölle der Rückrufe vermeiden können: Versprechen und ein hübsches Muster async/await. Ein Versprechen ist im Wesentlichen ein sofortiger Rückgabewert, der verspricht, einen bestimmten Wert später zurückzugeben.



//  ,  
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
    [...]
}

//     
function getActiveProfiles(): Promise<UserProfile[]> {
    return fetchUserProfiles(URL)
        .then(profiles => profiles.filter(profile => profile.active))
        .catch(error => handleError(error));
}


Da Anweisungen .then()in beliebiger Anzahl verkettet werden können, kann das obige Muster in einigen Fällen zu verwirrendem Code führen. Indem Sie eine Funktion deklarieren asyncund verwenden, awaitwährend Sie auf die Auflösung des Versprechens warten, können Sie denselben Code in einem viel synchroneren Stil schreiben. Auch in diesem Fall bietet sich die Möglichkeit, bekannte Operatoren einzusetzen try/catch:



//  async/await ( ,  fetchUserProfiles  )
async function getActiveProfiles(): Promise<UserProfile[]> {
    const allProfiles = await fetchUserProfiles(URL);
    return allProfiles.filter(profile => profile.active);
}

//   try/catch
async function getActiveProfilesSafe(): Promise<UserProfile[]> {
    try {
        const allProfiles = await fetchUserProfiles(URL);
        return allProfiles.filter(profile => profile.active);
    } catch (error) {
        handleError(error);
        return [];
    }
}


Beachten Sie, dass der obige Code zwar synchron zu sein scheint, aber nur sichtbar ist (da hier ein weiteres Versprechen zurückgegeben wird).



Extension Operator und Rest Operator: Erleichtern Sie Ihr Leben



Bei Verwendung von Java erzeugen Datenmanipulation, Konstruktion, Zusammenführung und Destrukturierung von Objekten häufig stereotypen Code in großen Mengen. Klassen müssen definiert, Konstruktoren, Getter und Setter generiert und Objekte instanziiert werden. In Testfällen ist es häufig erforderlich, aktiv auf Scheininstanzen geschlossener Klassen zurückzugreifen.



In TypeScript kann all dies mühelos mit seinem süßen typsicheren syntaktischen Zucker erledigt werden: Spread- und Rest-Operatoren.



Verwenden wir zunächst den Array-Erweiterungsoperator ..., um das Array zu entpacken:



const a = [ 'a', 'b', 'c' ];
const b = [ 'd', 'e' ];

const result = [ ...a, ...b, 'f' ];
console.log(result);

// >> [ 'a', 'b', 'c', 'd', 'e', f' ]


Es ist natürlich praktisch, aber das echte TypeScript beginnt, wenn Sie erkennen, dass Sie dasselbe mit Objekten tun können:



interface UserProfile {
    userId: string;
    name: string;
    email: string;
    lastUpdated?: Date;
}

interface UserProfileUpdate {
    name?: string;
    email?: string;
}

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const update: UserProfileUpdate = { email: 'bob@example.com' };

const updated: UserProfile = { ...userProfile, ...update, lastUpdated: new Date() };

console.log(updated);

// >> { userId: 'abc', name: 'Bob', email: 'bob@example.com', lastUpdated: 2019-12-19T16:09:45.174Z}


Mal sehen, was hier los ist. Grundsätzlich wird ein Objekt updatedmit dem Konstruktor für geschweifte Klammern erstellt. Innerhalb dieses Konstruktors erstellt jeder Parameter tatsächlich ein neues Objekt, beginnend von links.



Also wird das erweiterte Objekt verwendet userProfile; Das erste, was er tut, ist sich selbst zu kopieren. Im zweiten Schritt wird das erweiterte Objekt darin updatezusammengeführt und dem ersten Objekt neu zugewiesen. Dadurch wird wiederum ein neues Objekt erstellt. Im letzten Schritt wird das Feld zusammengeführt und neu zugewiesen lastUpdated, dann wird ein neues Objekt erstellt und als Ergebnis das endgültige Objekt.



Die Verwendung des Spread-Operators zum Erstellen von Kopien eines unveränderlichen Objekts ist eine sehr sichere und schnelle Methode zum Verarbeiten von Daten. Hinweis: Der Spread-Operator erstellt eine flache Kopie des Objekts. Elemente mit einer Tiefe von mehr als einem werden dann als Links kopiert.



Der Erweiterungsoperator hat auch ein Destruktoräquivalent namens Objektrest :



const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const { userId, ...details } = userProfile;
console.log(userId);
console.log(details);

// >> 'abc'
// >> { name: 'Bob', email: 'bob@example.com' }


Jetzt ist es an der Zeit, sich zurückzulehnen und sich den gesamten Code vorzustellen, den Sie in Java schreiben müssten, um die oben gezeigten Vorgänge auszuführen.



Fazit. Ein wenig über die Vor- und Nachteile



Performance



Da TypeScript von Natur aus asynchron ist und eine schnelle Laufzeitumgebung hat, gibt es viele Szenarien, in denen ein Node / TypeScript-Dienst mit einem Java-Dienst konkurrieren kann. Dieser Stapel eignet sich besonders für E / A-Vorgänge und eignet sich gut für gelegentliche kurze Blockierungsvorgänge, z. B. zum Ändern der Größe eines neuen Profilbilds. Wenn der Hauptzweck eines Dienstes jedoch darin besteht, ernsthafte Berechnungen auf der CPU durchzuführen, sind Node und TypeScript wahrscheinlich nicht sehr gut dafür geeignet.



Nummerntyp



Der in TypeScript verwendete Typ lässt ebenfalls zu wünschen übrig number, was nicht zwischen Ganzzahl- und Gleitkommawerten unterscheidet. Die Praxis zeigt, dass dies in vielen Anwendungen absolut kein Problem darstellt. Es ist jedoch am besten, TypeScript nicht zu verwenden, wenn Sie eine App für ein Bankkonto oder einen Checkout-Service schreiben.



Ökosystem



Angesichts der Popularität von Node.js sollte es keine Überraschung sein, dass es heute Hunderttausende von Paketen dafür gibt. Da Node jedoch jünger als Java ist, haben viele Pakete nicht so viele Versionen überlebt, und die Qualität des Codes in einigen Bibliotheken ist eindeutig schlecht.



Unter anderem sind einige hochwertige Bibliotheken zu erwähnen, mit denen man sehr bequem arbeiten kann: zum Beispiel für Webserver , Abhängigkeitsinjektion und Controller-Annotationen . Wenn der Dienst jedoch ernsthaft von zahlreichen und gut unterstützten Programmen von Drittanbietern abhängt, ist es besser, Python, Java oder Clojure zu verwenden.



Beschleunigte Funktionsentwicklung



Wie wir oben gesehen haben, ist einer der wichtigsten Vorteile von TypeScript, wie einfach es ist, komplexe Logik, Konzepte und Operationen in dieser Sprache auszudrücken. Die Tatsache, dass JSON ein wesentlicher Bestandteil dieser Sprache ist und heutzutage häufig als Datenserialisierungsformat für die Datenübertragung und die Arbeit mit dokumentenorientierten Datenbanken verwendet wird, scheint in solchen Situationen selbstverständlich, auf TypeScript zurückzugreifen. Das Einrichten eines Knotenservers ist sehr schnell, normalerweise ohne unnötige Abhängigkeiten. Dadurch werden Systemressourcen gespart. Aus diesem Grund ist die Kombination von Node.js mit dem starken Typensystem von TypeScript so effektiv, dass in kürzester Zeit neue Funktionen erstellt werden können.



Schließlich ist TypeScript gut mit syntaktischem Zucker aromatisiert, sodass die Entwicklung damit gut und schnell ist.



All Articles