GETYPTES JAVASCRIPT FÜR JAVA-ENTWICKLER - WILLKOMMEN BEI TYPESCRIPT - SIGS DATACOM
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Schwerpunktthema Willkommen bei TypeScript nützliche Features, die große JavaScript-Codebasen verwaltbar werden lassen. Getyptes JavaScript Viele Funktionen kennt man bereits aus ECMAScript 6. TypeScript nimmt hier eine Vorreiterstellung ein, indem es noch nicht vorhandene, aber von der Community gewünschte für Java-Entwickler Konzepte schon jetzt zur Verfügung stellt. Dieser Trend setzt sich seit der Veröffentlichung kontinuierlich fort und zieht da- durch immer mehr Entwickler an. Johannes Dienst Große JavaScript-Codebasen lassen sich nur schwer beherrschen. Selbst Ein kleines Beispiel mit modernen Entwicklungsumgebungen werden die Grenzen schnell erreicht. Das liegt schon in der Natur von JavaScript selbst, welche eine Ein erstes kleines Beispiel ist in Listing 1 zu finden. Es zeigt die dynamisch typisierte Skriptsprache ist. TypeScript hält Objektorientie- Verwendung des Klassensystems und von statischer Typisie- rung durch Interfaces und Klassen sowie optionale statische Typisierung rung. Die öffentliche Variable greeting ist vom Typ string. Unser bereit, wodurch nützliche Werkzeugunterstützung möglich wird. Wei- Roboter grüßt uns, wenn wir die kompilierte JavaScript-Datei tere sinnvolle Sprachfeatures machen TypeScript zur idealen Sprache in ein gültiges HTML-Dokument einbinden. für JavaScript-Anwendungen mit umfangreichen Codebasen. Als echte Obermenge von JavaScript ist der Umstieg sogar fast schmerzfrei. class MyRobot { constructor(public greeting: string) { } greet() { Getyptes JavaScript mit Zusatzfeatures return "" + this.greeting + ""; } } Bei der Entwicklung einer JavaScript-Applikation kommt E man ab einer bestimmten Größe der Codebasis früher var myRobot = new MyRobot("Hello, I'm your little robot!"); var str = myRobot.greet(); oder später an den Punkt, an dem nur mit höchster Diszip- document.body.innerHTML = str; lin und großer Konzentration der Überblick behalten werden kann. Oft führt der Weg in die Unwartbarkeit des Codes und Listing 1: TypeScript-Beispiel MyRobot wehe, der Verantwortliche soll einen Kollegen mit dem Projekt vertraut machen. Bei der Instantiierung der Klasse MyRobot wird also ein String An dieser Weggabelung stehen viele Projekt und die Betei- erwartet. In klassischem JavaScript würde eine Anweisung wie ligten suchen nach einem geeigneten Ausweg. Einerseits ist new MyRobot(2) keinen Fehler hervorrufen. In TypeScript wird be- JavaScript eine flexible Sprache, die in den richtigen Händen reits vor der Kompilierung ein Fehler angezeigt. Damit können sinnvolle Ergebnisse hervorbringt. Andererseits sprechen Kon- eventuelle Fehler schon vor der Ausführung erkannt werden. strukte wie Pseudo-OOP dafür, dass es der Sprache an manchen Natürlich stellt sich die Frage, wie man bei der Migration Stellen an wichtigen Konzepten mangelt. Auf der Suche nach einer bestehenden Codebasis vorgeht. Dafür gibt es die Mög- einer Alternative gibt es nicht sehr viel Auswahl; Dart und Cof- lichkeit, die Typisierung ganz wegzulassen oder den Typ any feeScript sind interessant, aber auch mit einer deutlichen Lern- zu verwenden. Listing 2 zeigt den vom TypeScript-Compiler kurve behaftet. Außerdem wird dann die Entwicklungssprache generierten JavaScript-Code, der auch so in einer ts-Datei von für immer festgelegt, da das aus ihnen generierte JavaScript ihm akzeptiert werden würde. Es wurden alle Typinformati- nicht dem entspricht, was ein normaler Entwickler erzeugen onen entfernt, sodass ein Aufruf wie new MyRobot(314) genauso würde. TypeScript tritt an, sich dieses Problems anzunehmen. gültig wäre. Von der Syntax her an Java mit einer Prise Scala erinnernd, verspricht es echte Objektorientierung und statische Typisie- var MyRobot = (function () { rung, falls gewünscht. Außerdem ist der Compiler zu reinem function MyRobot(greeting) { JavaScript Open Source. Die Sprache überrascht nach einer this.greeting = greeting; kurzen Eingewöhnung mit einer angenehmen Lernkurve und } MyRobot.prototype.greet = function () { einfach zu installierender Werkzeugunterstützung. Der inzwi- return "" + this.greeting + ""; schen flotte Compiler generiert ansehnlichen Code, der den }; gängigen Formatierungsrichtlinien entspricht. return MyRobot; })(); var myRobot = new MyRobot("Hello, I'm your little robot!"); Entstehungsgeschichte var str = myRobot.greet(); document.body.innerHTML = str; TypeScript wurde 2012 von Microsoft unter der Apache-Lizenz als Open Source entwickelt. Anders Hejlsberg erklärt im Ein- Listing 2: Vom TypeScript-Compiler generierter JavaScript-Code führungsvideo auf der Projektseite [Typ], dass die Entwick- lung von JavaScript ab einer gewissen Größe nur mit speziellen Tools möglich sei, die nicht in das JavaScript-Ökosystem einge- Werkzeugunterstützung bunden seien. Dadurch verliere man den komfortablen Zugriff auf vorhandene Bibliotheken. Beim Tooling hat sich seit der Veröffentlichung einiges getan. Deswegen wurde TypeScript entwickelt, das zu sauber for- Microsoft Visual Studio unterstützt TypeScript inzwischen in matiertem reinen JavaScript kompiliert wird und somit über- vollem Umfang und wird als First-Class-Member über eine Er- all lauffähig ist. Durch die Abstraktion ist sinnvolles Tooling weiterung mit Stand-alone-Compiler gehandhabt (s. Abb. 1). möglich. Ebenso optionale statische Typisierung und weitere Dadurch entfällt eine etwaige Node.js-Installation. www.javaspektrum.de 25
Schwerpunktthema sind noch nicht genug? Im Folgenden werden ausgewählte Sprachfeatures dargestellt. Bei richtigem Einsatz machen sie die Codebasis verständlicher und damit wartbarer. Interfaces mit struktureller Typisierung Interfaces funktionieren in TypeScript ziemlich ähnlich wie in Java. Natürlich gibt es Besonderheiten wie optionale Member- variablen oder Hybridtypen. Im normalen Entwickleralltag wird man diesen aber nicht sehr oft begegnen. Größter Unterschied zu Java ist die strukturelle Typisierung, wie sie in Listing 3 zu sehen ist. Ein Interface beschreibt also die Struktur einer Klasse, was es unnötig macht, dass die jewei- lige Klasse ihre implementierten Interfaces explizit benennt. Der Aufruf der Funktion greetTheAudience mit einer Instanz von MyRobot wird vom Compiler nicht bemängelt, da die Methode greet vorhanden ist. interface Robot { greet(): string; } class MyRobot { constructor(public greeting: string) { } greet(): string { Abb. 1: Umfangreiches Tooling in Visual Studio 2013 return "" + this.greeting + ""; } }; function greetTheAudience(robot: Robot) { Für Webstorm, Intellij und Eclipse wird eine Node.js-Instal- var str = robot.greet(); lation benötigt. Zusätzlich sind noch die nötigen Plug-ins zu document.body.innerHTML = str; } installieren. Das wären bei Eclipse lediglich TypEcs [TEcs] und var myRobot = new MyRobot("Hello, I'm your little robot!"); bei Webstorm/Intellij JavaScript Support und Node.js [Jet]. greetTheAudience(myRobot); In Eclipse muss anschließend einem Projekt noch die Type- Script-Umgebung hinzugefügt werden. Erst dann unterstützt Listing 3: Strukturelle Typisierung die IDE sinnvolles Syntaxhighlighting, Code-Vervollständi- gung und das bequeme Autokompilieren von Dateien mit En- Mixins dung ts (s. Abb. 2). Zusätzlich zu strukturellen Vererbungshierarchien gibt es noch ein weiteres mächtiges Werkzeug, das sich für Vererbung in Be- tracht ziehen lässt. Mit Mixins kann man Klassen auch ohne Ab- Weitere wichtige Sprachfeatures leitung mit Eigenschaften versehen. Komplizierte Vererbungs- hierarchien können damit manchmal radikal vereinfacht werden. Eine optionale statische Typisierung und Objektorientierung In Listing 4 werden zuerst die Mixins Walk und Fly definiert, über Vererbung von Eigenschaften sowie hilfreiches Tooling die anschließend einem Roboter hinzufügt werden. Die Kon- solenausgabe bestätigt, dass der er- zeugte Roboter tatsächlich laufen und fliegen kann. Der Vorteil dieser Vorgehensweise ist klar. Soll ein Ro- boter erstellt werden, der nur eine der beiden Fortbewegungsmethoden be- herrscht, kann einfach nur diese hin- zufügt werden. Es ist keine Änderung an übergeordneten oder abgeleiteten Klassen dadurch vorzunehmen. Leider ist an dieser Stelle schon ein großer Nachteil von Mixins er- kennbar. Jede Methode (und auch Membervariable), die in eine Klas- se eingewebt werden soll, muss noch einmal in der Klasse selbst als Stumpf deklariert werden. Dadurch ergibt sich ein nicht zu unterschät- zender Overhead. Bis jetzt wurde dieser bekannte und von der Com- munity schon bemängelte Umstand noch nicht angegangen. Hier sollte Abb. 2: TypeScript-Tooling in Eclipse vielleicht ein spezielles Schlüssel- wort eingefügt werden oder das 26 JavaSPEKTRUM 4/2015
Schwerpunktthema Externe Module verwenden entweder CommonJS oder // Walk Mixin AMD als Laufzeitlademechanismus. Die Entscheidung über class Walk { walk() { die Art des Mechanismus wird über das Compilerflag -module console.log("I walk"); gefällt. Eine detaillierte Erörterung zu externen Modulen wird } an dieser Stelle nicht gegeben. } Wieder zurück zu den für TypeScript spezifischen internen // Fly Mixin Modulen. Dazu wird in Listing 5 eine kleine Fabrik für die class Fly { fly() { Roboter aus den vorherigen Beispielen entwickelt. Die Um- console.log("I fly"); setzung erfolgt als internes Modul. Das Schlüsselwort module } leitet das Modul ein. Alles, was mit dem Schlüsselwort export } beginnt, ist von außen sichtbar. So sollen das Interface Robot class MyRobot implements Walk, Fly { constructor() { und die Factory-Methode zur Erzeugung neuer Roboter nicht setInterval(() => this.walk(), 1000); abgeschottet werden. Die Membervariable standardMessage und } die eigentliche Klasse MyRobot sind nur innerhalb des Moduls walk: () => void; sichtbar. fly: () => void; } // Verwebung der Mixins mit der Klasse module NiceRobot { applyMixins(MyRobot, [Walk, Fly]) var standardMessage = "I am a nice robot"; var robot = new MyRobot(); export interface Robot { setTimeout(() => robot.fly(), 1000); greet(): string; } // Funktion zur Verwebung der Klasse mit den Mixins class MyRobot implements Robot { function applyMixins(derivedCtor: any, baseCtors: any[]) { constructor(public greeting: string) { } baseCtors.forEach(baseCtor => { greet(): string { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { return this.greeting; derivedCtor.prototype[name] = baseCtor.prototype[name]; } }) } }); export function constructRobot(message?: string ): Robot { } if (message) { return new MyRobot(message); } else { return new MyRobot(standardMessage); } Listing 4: Vererbung mit Mixins } } Schlüsselwort implements zur Erkennung von Mixins überladen werden, wie bereits vorgeschlagen wurde (vgl. [Mix]). Dann Listing 5: Verwendung eines internen Moduls würde der Compiler die duplizierte Angabe nicht benötigen. Um auf das Modul von einer anderen Datei aus zugreifen Module zu können, wird es zuerst mit einer Referenz eingebunden. In TypeScript gibt es zur Modularisierung des Codes Modu- Ist es über mehrere Dateien verteilt, dann müssen alle Dateien le. Dabei wird zwischen internen und externen Modulen un- referenziert werden. Anschließend erzeugt der Aufruf NiceRo- terschieden (vgl. Kasten „Interne und externe Module“). Inter- bot.constructRobot("Asta la Vista") einen neuen Roboter. Listing 6 ne Module können sich dabei über mehrere Dateien erstrecken zeigt das komplette Beispiel, wobei davon ausgegangen wird, und dienen dem Zweck, den globalen Namespace nicht zu ver- dass die Datei im selben Ordner abgelegt wurde wie die Mo- stopfen. Die Wahrscheinlichkeit von Namenskollisionen wird duldatei. damit minimiert. Sie können jedoch nicht zur Laufzeit geladen werden, da kein Mechanismus dafür zur Verfügung steht. Des- /// wegen müssen entweder alle generierten Dateien per Hand var Arnold: NiceRobot.Robot = eingebunden werden oder der Compiler erhält die Anweisung, NiceRobot.constructRobot("Asta la vista"); alles in eine Datei zu verpacken. alert(Arnold.greet()); Listing 6: Modulzugriff Interne und externe Module Optionale Parameter mit Standardbelegung Interne Module bezeichnen im Grunde genommen In Java können optionale Parameter nur über das varargs-Kon- nichts Anderes als einen separaten Namespace. Dass strukt (zum Beispiel: String... strings) als Parameternamen sie mit dem Schlüsselwort module deklariert werden, stif- übergeben werden. Dabei werden die Parameter in ein Array tet Verwirrung. Von einem Modul erwartet man eigent- umgewandelt, was zusätzlichen Overhead innerhalb der Me- lich, dass es mit einer einzigen Zeile eingebunden wer- thode bedeutet. In JavaScript sind optionale Parameter keine den kann. Zum Beispiel mit require(‘modulname’), wie bei Seltenheit. Beiden Sprachen gemeinsam ist die nicht vorhande- AMD-Modulen. Das ist bei internen Modulen nicht der ne Standardbelegung, wenn ein Parameter beim Aufruf nicht Fall. Dort muss unter Umständen jede Einzeldatei mit angegeben wird. In Java ist das Array leer und in JavaScript einer Referenz im Kopfbereich der TypeScript-Datei an- ist die betreffende Variable undefined. TypeScript enthält so- gegeben werden. wohl optionale Parameter als auch eine mögliche Standard- Tatsächlich gibt es für TypeScript 1.5 den Vorschlag, belegung. das Schlüsselwort namespace für interne Module einzu- Listing 7 veranschaulicht deren Verwendung. Vor allem führen. Zum Zeitpunkt der Erstellung dieses Artikels Default-Parameter können zur Lesbarkeit des Codes erheblich war erst die Version 1.4 verfügbar. beitragen, da sie Evaluationen von bedingten Auswertungen am Anfang der Methode überflüssig machen. www.javaspektrum.de 27
Schwerpunktthema Abhilfe schafft TypeScript mit einem sogenannten Lamb- // Funktion mit optionalen Parameter daausdruck. In Listing 9 sorgt die angepasste Funktion dafür, function printGreetingMessageRobot(robot?: Robot) { dass this bereits bei der Erzeugung der Funktion festgelegt var str = "I am no robot..."; if (robot) { wird. Dadurch verhält sich this so, wie man es intuitiv erwarten str = robot.greet(); würde. Mit der Änderung von Listing 9 wird die gewünschte } Alert-Box angezeigt. document.body.innerHTML = str; } // Funktion mit default Parameter function printGreetingMessage(message = Externe Bibliotheken "I am your function") { document.body.innerHTML = message; Ohne externe Libraries und Frameworks geht heutzutage in } JavaScript nichts mehr. Da TypeScript ein Typsystem verwen- var myRobot = new MyRobot("Hello, I'm your little robot!"); printGreetingMessageRobot(myRobot); det, werden Definitionsdateien mit der Endung *.d.ts benötigt. // Ausgabe: Hello, I'm your little robot! Ansonsten erkennt der Compiler zum Beispiel die Funktion $ printGreetingMessageRobot(); // Ausgabe: I am no robot... aus jQuery nicht, da ihm der entsprechende Typ fehlt. Bei ei- printGreetingMessage(); // Ausgabe: I am your function ner Zeile wie Listing 7: Optionale Parameter $("#body").innerHtml("I am from jQuery") Lambdas und this würde er die Kompilierung schlicht und einfach verweigern. Als JavaScript-Neuling und vor allem als Java-Entwickler stif- Das Projekt stößt aber auf große Gegenliebe bei Entwick- tet die Funktionsweise von this des Öfteren Verwirrung. Das lern und Unternehmen, sodass sich mit dem Projekt Defi- liegt hauptsächlich daran, dass die Objektorientierung von Ja- nitelyTyped [Def] eine Anlaufstelle für Definitionsdateien vaScript über Prototypen realisiert ist und damit this nicht die gebildet hat. Dort können Definition Requests abgegeben aktuelle Instanz einer Klasse bezeichnen kann. Das Schlüsselwort werden und es befinden sich dort inzwischen Definitions- this bezeichnet vielmehr den aktuellen Aufrufkontext, was in Ja- dateien für alle gängigen Bibliotheken. Das dazugehörige vaScript nicht unbedingt das beinhaltende Objekt sein muss. GitHub-Repository umfasst zur Zeit der Erstellung dieses So einfach diese Definition klingt, so schwierig kann sie in Artikels mehr als 700 Definitionsdateien und es werden fast der Praxis sein. Listing 8 zeigt ein Beispiel aus dem TypeScript- täglich mehr. Handbuch [Typ]. Beim Aufruf von cardPicker() zeigt this nicht Eine d.ts-Datei wird als Referenz mit der bekannten Type- auf das übergeordnete Objekt desk, sondern laut Definition auf Script-Syntax in Dateien eingebunden, die die entsprechende das aufrufende Element window (im strikten Modus sogar auf un- Bibliothek verwenden wollen. Um bei jQuery zu bleiben, folgt defined). Wird der Code so ausgeführt, ist die Folge eine Feh- an dieser Stelle die dazugehörige Zeile: lermeldung, da this.suits nicht definiert ist. Eine Lösung wäre, den gewünschten Kontext in einer Variablen that in desk abzu- /// legen. Damit kann das eine Ebene tiefer gelegene Objekt create- CardPicker darauf zugreifen. Aber schön und elegant ist so etwas Typischerweise wird die Referenz im Kopfbereich einer Type- natürlich nicht. Script-Datei eingebunden. Dann ist die Funktion $ dort unein- geschränkt nutzbar. var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), Praxiserfahrung createCardPicker: function() { return function() { Wie sieht die Arbeit mit TypeScript in der Praxis aus? In ei- var pickedCard = Math.floor(Math.random() * 52); nem kleinen, aber dennoch komplexen Projekt, mit abschlie- var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; ßend über 3000 Zeilen Code, hat sich TypeScript als sehr nütz- } lich erwiesen. Dort wurde zuerst mit reinem JavaScript entwi- } ckelt, was sich zunehmend als unüberschaubar herausstellte. } Die Herausforderung bestand darin, asynchrone Methoden- var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); aufrufe mit Promises aus der Bibliothek Q und DOM-Mani- pulationen mit jQuery zu implementieren. Durch das fehlende alert("card: " + pickedCard.card + " of " + pickedCard.suit); Tooling wurden die geschachtelten Aufrufhierarchien schwer nachvollziehbar. Änderungen waren schon in der fortgeschrit- Listing 8: Das Schlüsselwort this in JavaScript verhält sich unerwartet tenen Anfangsphase mühsam und fehlerträchtig. Der Einsatz von Interfaces und zu weiten Teilen statischer Ty- ... pisierung ging leicht, aber nicht ohne Aufwand, von der Hand. createCardPicker: function() { Am Ende standen klar definierte Hierarchien sowie Funktio- // Lambdaausdruck legt this bei der Erzeugung fest nen. Die vom Compiler durchgeführte Typprüfung brachte so return () => { var pickedCard = Math.floor(Math.random() * 52); manchen schlummernden Fehler ans Tageslicht. Diese Spezial- var pickedSuit = Math.floor(pickedCard / 13); fälle wären erst sehr spät und zur Laufzeit aufgetreten. Schon return {suit: this.suits[pickedSuit], card: pickedCard % 13}; alleine dafür lohnt sich der Aufwand. } Für die beiden eingesetzten Bibliotheken stehen vollständi- } ... ge Definitionsdateien zur Verfügung, die von hoher Qualität sind. Lediglich in Q konnte eine Funktion nicht wie gewohnt Listing 9: Lambdas legen den Scope von this bei der Erzeugung fest benutzt werden. 28 JavaSPEKTRUM 4/2015
Schwerpunktthema Insgesamt erinnert die Codebasis an Java-Code, sodass sie Die persönliche Praxiserfahrung des Autors hat gezeigt, dass von großen Teilen der Entwickler im Unternehmen leicht ver- TypeScript eine ernst zu nehmende und angenehm zu verwen- standen werden kann. Als Bonus wurde an Stellen, an denen dende Sprache ist. Eine aus dem Ruder laufende JavaScript- reines JavaScript ausdrucksstärker und mächtiger ist (Stich- Anwendung konnte damit wieder verwaltbar gemacht wer- wort: funktionale Programmierung) einfach darauf zurückge- den. Das Ziel, JavaScript im Applikationsumfang wartbar und griffen. möglich zu machen, hat Microsoft damit erreicht. Zusammenfassung und Ausblick Links und Literatur TypeScript ist für sein junges Alter eine sehr ausgewachsene [Def] DefinitelyTyped, TypeScript-Definitionsdateien für JS- Sprache mit stetig wachsender Community. Die Entwickler ori- Libraries, http://definitelytyped.org/ entieren sich an den ECMAScript-Standards, was das erwor- [Jet] Jetbrains, TypeScript-Support in IntelliJ IDEA, bene Wissen sogar auf reines JavaScript übertragbar macht. https://www.jetbrains.com/idea/help/typescript-support.html Bis auf kleinere Schwächen der Sprache enthält sie eine ganze [Mix] Microsoft, Dokumentation Mixins, Reihe wichtiger Abstraktionen und Vereinfachungen, wie Ver- https://typescript.codeplex.com/wikipage?title=Mixins%20in%20TypeScript erbung mit Interfaces, Lambdaausdrücke, ein Modulkonzept [Msd] J. Turner, Angular 2: Built on TypeScript, 2015, und noch mehr. http://blogs.msdn.com/b/typescript/archive/2015/03/05/angular- Mit diesem Werkzeugkasten ist die Entwicklung von Ja- 2-0-built-on-typescript.aspx vaScript-Anwendungen in großem Stil und ohne ins Chaos [TEcs] TypEcs, TypeScript IDE for Eclipse, http://typecsdev.com/ abzugleiten möglich. Das wird dadurch bestätigt, dass bereits [Typ] TypeScript, Projektseite, http://www.typescriptlang.org größere Unternehmen und Entwickler von Frameworks auf TypeScript setzen. Erst im März hat AngularJS angekündigt, die Version 2.0 mit TypeScript zu entwickeln [Msd]. Auch das entstandene Ökosystem um TypeScript mit seiner aktiven Community zeigt, dass ein breites Interesse von Ent- wicklerseite besteht. Dem Projekt DefinitelyTyped werden wö- chentlich neue Definitionsdateien hinzugefügt, mit denen sich Johannes Dienst ist Softwareentwickler bei der JavaScript-Bibliotheken nahtlos in TypeScript einbinden lassen. MULTA MEDIO Informationssysteme AG in Würzburg. Er ist leidenschaftlicher Java-Entwickler, wobei ihm Die Roadmap bis zum Release 2.0 liest sich ebenfalls viel- das Thema Clean Code sehr am Herzen liegt. In seiner versprechend. So soll TypeScript an ECMAScript 6 angeglichen Freizeit beschäftigt er sich gerne mit allen Themen werden, um als Obermenge für die nächsten Versionen zur Ver- rund um Webentwicklung. fügung zu stehen. Dazu werden noch fehlende Features, wie E-Mail: jdienst@multamedio.de Destrukturierung, Promises oder Iteratoren, implementiert. Außerdem wird das Engagement innerhalb der JavaScript- Gemeinschaft weitergeführt. So sollen weitere Kooperationen entstehen, um TypeScript weiterzuentwickeln. www.javaspektrum.de 29
Sie können auch lesen