Podcast

TypeScript

Was sind das denn für Typen?

TypeScript ist in aller Munde. In dieser Folge will Lucas von Lars wissen, wieso er sich so sehr für diese Sprache begeistert. Wie ordnet ein erfahrener Scala-Entwickler wie Lars das Typsystem von TypeScript ein? Lars beschreibt zudem, welche Vorteile ein mächtiges Typsystem mit sich bringt, und erklärt, wie die Interaktion mit JavaScript-Bibliotheken funktioniert.
Listen to other episodes

Shownotes & Links

Transkript

show / hide transcript

Lucas Dohmen: Hallo und herzlich Willkommen zu einer neuen Folge des INNOQ Podcast. Heute habe ich mir den Lars eingeladen. Hallo, Lars!

Lars Hupel: Hallo, Lucas!

Lucas Dohmen: Wir haben uns heute das Thema “TypeScript” ausgesucht und zwar aus einer etwas anderen Perspektive. Bevor wir aber damit loslegen, erst einmal zu dir, Lars: Wer bist du und was machst du bei INNOQ?

Lars Hupel: Ich bin Lars und bin Consultant bei INNOQ. Ich bin seit etwas über einem Jahr hier. Ich mache eigentlich viele verschiedene Sachen, bin so ein bisschen ein “Allrounder”. In der Zeit bei INNOQ habe ich mich mit Blockchain beschäftigt, dem Schulungsgeschäft, Java Backend-Themen und bin derzeit eher im Frontend-Bereich unterwegs.

Lucas Dohmen: Bei Lars ist es so, dass er jetzt noch nicht so super viel JavaScript-Erfahung hat. Ich würde sagen, es ist fair, oder?

Lars Hupel: Ja, auf jeden Fall.

Lucas Dohmen: Trotzdem ist er jetzt Vollzeit-JavaScript-Entwickler geworden und seine Perspektive auf TypeScript ist eher die von einem Scala-Entwickler. Ich glaube, davon können wir jetzt viel lernen, wie er diese Sprache so sieht. Bevor wir dazu kommen, sag doch erst einmal kurz: Was ist dieses TypeScript überhaupt?

Lars Hupel: TypeScript ist letztendlich eine syntaktisch sehr ähnliche Sprache zu JavaScript und hat zusätzlich noch Typen. Man kann im Prinzip ganz gewöhnlichen JavaScript Code schreiben. Die Syntax sieht genauso aus, die Keywords sind genauso. Es gibt die gleichen Feautures, Classes, Functions und so weiter. Man kann zusätzlich noch Typen dranschreiben und es gibt noch ein paar extra Features obendrein, wie man sie vielleicht aus Java gewohnt ist. Zum Beispiel die Sichtbarkeit von Properties. Ansonsten, wenn man es auf eine Phrase runterdampfen wollen würde, wäre es: JavaScript plus Typen.

Lucas Dohmen: Was passiert dann mit JavaScript? Kommmt da ein Compiler oder gibt es einen eigenen Interpreter? Wie funktioniert das?

Lars Hupel: Der gängige Ansatz ist, dass man den TypeScript-Code entweder mit dem offiziellen TypeScript-Compiler nach JavaScript kompiliert oder transpiliert, wie man in der Community sagt. Man kann aber auch so etwas wie “Babel” oder “Sucrase” oder alle möglichen anderen Transpiler, die auch TypeScript-Support haben, nutzen. Da gibt es ganz viele verschiedene Möglichkeiten, sich in das normale JavaScript-Ökosystem einzugliedern. Wenn man schon eine existierende Tool-Chain hat, wie z.B. Webpack oder Babel, kann man dann einfach das TypeScript-Plug-in laden und dann funktionieren die Tools mit TypeScript genauso wie mit JavaScript.

Lucas Dohmen: Bis auf ein paar kleine Ausnahmen ist es, glaube ich, wirklich nur das, was in der ECMAScript-Spezifikation steht, plus die Typen, die nicht in der Spezifikation stehen. Vielleicht sogar auch ein paar Feautures, die in aktuellen Browsern noch nicht funktionieren, die dann aber rückwärtskompatibel kompiliert werden, richtig?

Lars Hupel: Genau, richtig. Wenn man den offiziellen TypeSript-Compiler benutzt, kriegt man auch das Type-Checking. Das nutzt man meistens im Development-Modus, sodass man auch die Type-Errors angezeigt bekommt. Es gibt auch die Möglichkeit, es on-the-fly zu transpilieren. Das kann sinnvoll sein, wenn man irgendetwas direkt im Browser ausführen will. Es gibt mittlerweile auch Tools, die man vor Node.js hängen kann und es dann auch on-the-fly kompiliert. Da gibt es ganz viele verschiedene Möglichkeiten, es dann letztendlich auszuführen. Allen gemein ist, dass sie irgendwie JavaScript erzeugen, was dann ausgeführt wird.

Lucas Dohmen: Es ist also keine eigene Engine geschrieben worden oder so etwas.

Lars Hupel: Genau.

Lucas Dohmen: Es ist eine Sprache, die dazu gedacht ist, zu JavaScript kompiliert zu werden. Bevor wir jetzt genau in Typsysteme einsteigen, für so ein grobes Bauchgefühl zur Einordnung: Ist es eher so ein Typsystem, wie das von Java oder eher wie von Scala? Wo steht es da ungefähr?

Lars Hupel: Ich würde sagen, dass TypeScript ein relativ komplexes Typsystem hat, wahrscheinlich eher in die Scala-Ecke geht und davon auch einige Inspiration her hat. Der Hintergrund ist, dass die Leute – in dem Fall Microsoft, da TypeScript von Microsoft entwickelt wurde – gesagt haben, dass in JavaScript ganz viele verschiedene Dinge gemacht werden. Sie haben sich also die existierenden JavaScript-Bibliotheken angeschaut und geguckt, welche Muster und Programmierpatterns verwendet werden und wie man es schaffen könnte, sie in einem Typsystem zu modellieren. Also ganz spannend, denn die allermeisten getypten Programmiersprachen fangen damit an, sich das Typsystem zu überlegen und daraus folgt dann, welche Patterns man benutzt. Bei TypeScript war es eben genau umgedreht: Man hat erst einmal JavaScript hergenommen und geguckt. Dabei hat sich zum Beispiel herausgestellt, dass es bestimmte Bibliotheken gibt. Da gibt es Funktionen, die einen String und eine Funktion annehmen – wie z.B. die Event-Handler in Node.js. Und je nachdem, welchen String man übergibt, je nachdem auf welches Event man listen will, hat die Funktion an der Stelle einen anderen Typ. Da hat man überlegt, dass man es also schaffen muss, auf Basis eines Strings einen Typ zu entscheiden. Viele von diesen Dingen sind also darin, die in Typsystemen relativ fortgeschritten sind und was viele Programmiersprachen nicht haben. Zum Beispiel hat Java solche Sachen einfach nicht. Man hat aber wirklich danach modelliert, welche existierenden Patterns es in JavaScript gibt und die sind leider dummerweise relativ komplex. Deswegen würde ich auch sagen, dass TypeScript vom Typsystem her deutlich komplexer ist als Java. Gleichzeitig ist es aber auch extrem benutzbar, weil sie es geschafft haben, es in existierendenes Tooling so zu integrieren, dass man sehr selten in irgendwelche bizarren Use-Cases reinläuft, wo man dann gar nicht mehr weiß, was man tun muss. Meiner Ansicht nach, haben sie die Integration in die existierenden Bibliotheken sehr gut hinbekommen.

Lucas Dohmen: Wenn ich jetzt den Begriff “Typsystem” höre, was bedeutet das eigentlich? Was ist ein Typsystem?

Lars Hupel: Ein Typsystem legt fest, welche Operationen man auf welchen Werten ausführen kann, also welche Ergebnisse dabei herauskommen können. Wenn ich mir z.B. den “+”-Operator anschaue, dann kann er in Java zwei Integer annehmen und gibt einen neuen Integer zurück. Wenn er zwei Strings nimmt, gibt er einen String zurück. In bestimmten Typsystemen ist es sehr eingeschränkt: In Sprachen wie Haskell z.B. kann man nur Integer auf Integer addieren und da kommt immer ein Integer heraus. In Systemen wie Java kann man auch einen String und einen Integer addieren. Dann wird der Integer zu String konvertiert. In Systemen wie JavaScript kann man eigentlich alles mit jedem addieren: “String + Object” und dann kommt eben irgendetwas heraus. Das ist im Regefall nicht das, was man will. Ein Typsystem würde jetzt einschränken, was man machen kann. Das müssen jetzt nicht nur Operatoren sein, sondern es können auch Klassen oder Funktionen, also alles Mögliche, sein. Man kann also erst einmal alle möglichen Ausdrücke hinschreiben, von denen nicht alle Sinn machen. Ein Typsystem hilft dabei, die Ausdrücke hinzuschreiben, die auch wirklich Sinn machen und diese abzulehnen, die keinen Sinn machen. Zum Beispiel “Object + Array”. So etwas macht eben gar keinen Sinn. Ein Typsystem würde so etwas eben statisch herausfiltern.

Lucas Dohmen: Wenn wir jetzt bei dem Bespiel “addieren” bleiben, dann ist es ja so, dass man in JavaScript wirklich alles mit allem addieren kann. Schränkt TypeScript einen da jetzt ein und verhindert Dinge, die man sonst in JavaScript eigentlich tun könnte? Zum Beispiel “Hallo + 5” zu rechnen, was in JavaScript “Hallo5” ergeben würde. Darf man das dann noch oder wird es dann von TypeScript verboten?

Lars Hupel: Es gibt bestimmte Sachen in TypeScript, die immer noch erlaubt sind und andere Sachen, die verboten sind. Der Fall “Object + Array” würde von TypeScript verhindert werden. Ich bin mir gerade nicht zu 100 Prozent sicher, was bei “String + Number” passieren würde. Es kann gut sein, dass es so etwas weiterhin zulässt. Es ist ein Trade-Off an vielen Stellen. Man kann z.B. eine bestimmte Menge an Type-Coercions hinnhemen und durchlassen und andere Sachen eben nicht.

Lucas Dohmen: Grudsätzlich kann es schon sein, dass manche Dinge nicht mehr erlaubt sind, die in JavaScript schon erlaubt wären?

Lars Hupel: Genau. Alle möglichen Sachen haben in JavaScript eine definierte Semantik. Wenn man wissen will, was “Object + Array” bedeutet, dann braucht man ja bloß in die Spezifikation hereinzuschauen oder man guckt, was Node oder Chrome macht. Das Programm crasht nicht, wenn man das macht. Trotzdem ist es so, dass man sich gesagt hat, dass es im Regelfall ein Fehler ist, denn es passiert extrem selten, dass man ein Object und ein Array aufeinander addieren will.

Lucas Dohmen: Verstanden. Ich stelle mir vor, ich möchte als Autor eine Bibliothek verwenden, die in “normalem” JavaScript geschrieben ist. Die hat dann ja erst einmal keine Typinformation. Wie funktioniert da jezt die Zusammenarbeit zwischen dieser statisch typsierten Welt in TypeScript und der JavaScript-Welt?

Lars Hupel: Es gibt in TypeScript so einen “Catch All”-Typen namens “Any”. Alles, wovon TypeScript keine Typen kennt, wird einfach als Any angenommen. Man kann sich also alle möglichen Bibliotheken reinziehen, ganz normal wie JavaScript benutzen und der TypeScript-Compiler würde nie meckern. Der TypeScript-Compiler wird immer sagen: Das ist zulässig, denn ich weiß nicht, was der Typ davon ist. Das kann jetzt eine Funktion, ein Objekt oder was auch immer sein. Er kann es nicht weiter feststellen. Glücklicherweise hat sich eine riesige Community zusammengesetzt für existierende JavaScript-Bibliotheken, die von mehr als drei Leuten benutzt werden, habe ich das Gefühl. Es ist wirklich fast alles drin und in einem Repo “DefinitelyTyped” zusammengetragen. DefinitelyTyped ist eine Menge von externen Typdefinitionen. Das heißt, man guckt sich jetzt irgendeine Bibliothek an. Magst du mal ein Beispiel für eine Bibliothek geben?

Lucas Dohmen: “ExpressJS” beispielsweise.

Lars Hupel: ExpressJS ist in JavaScript implementiert und hat keine eigene Typdefinition. Also kann man in dieses DefinitelyTyped Repo hineinschauen und stellt dann fest: Da gibt es eine Express-Typdeklaration. Mit “npm install @type/express” - wobei alle diesen “@type”-Präfix haben. Es wird dann automatisch vom TypeScript-Compiler erkannt, dass es da noch zusätzliche Typdefinitionen gibt. Damit würde der Compiler nun zu meckern anfangen, wenn man z.B irgendeine Route in Express einhängt, die eigentlich gar keine Route sondern irgendetwas anderes ist. Das würde dann erkannt werden. Wenn man diese Typdefinition aber nicht installiert hat, dann kann es sein, dass es zur Laufzeit schief geht.

Lucas Dohmen: Diese Typdefinitionen aus dem DefinitelyTyped sind also nicht Teil der Bibliothek selbst und werden nicht von dem Express-npm-Paket mit ausgeliefert, sondern sind ein separates Paket. Der Compiler weiß dann, dass diese zwei Sachen zusammengehören und benutzt deswegen die Typinformation aus diesem einen Paket für das andere.

Lars Hupel: Genau. Ich würde es allerdings noch etwas einschränken. Bei Express ist es der Fall, es ist getrennt. Es gibt die JavaScript-Implementierung und die ist unabhängig von der Typdefinition. Es gibt aber auch ganz viele Bibliotheken mittlerweile, die zwar immer noch in JavaScript implementiert sind, aber auch die Typdefinition selbst mitliefern. Da ist in einem npm-Paket nicht nur die “index.js” drin, sondern auch eine “index.d.ts” - Declaration Punkt TypeScript. Die würde dann auch von TypeScript erkannt werden. Man muss also nicht dieses separate Typ-Paket installieren. Es passiert ganz oft, dass sich BibliotheksautorInnen die Typdefinition aus DefinitelyTyped klauen und in ihre eigene Bibliothek mitintegrieren und dann gemeinsam pflegen, weil sie feststellen, dass es so ganz cool ist. So etwas passiert schon auch.

Lucas Dohmen: Das bedeutet, dass der Autor nicht unbedingt etwas darüber wissen muss, aber wenn er es tut, dann könnte er es mitausliefern.

Lars Hupel: Genau.

Lucas Dohmen: Wenn ich jetzt eine Bibliothek habe, die nicht getypt ist und ich möchte so eine Defintion schreiben, ist das dann ein manueller Prozess? Muss ich mich dann hinsetzen, die gesamte API durchgucken und bei jeder API-Sache dranschreiben: Das ist der Typ davon und das ist der Typ davon? Wie muss ich mir das vorstellen?

Lars Hupel: Genau, entweder guckt man in der Doku nach und wenn die Doku lügt, muss man in den Source-Code schauen. Tatsächlich ist es so, dass ganz viele JavaScript-Dokumentationen oder Bibliotheksdokumentationen in JavaScript sowieso schon irgendeine Art von Typ hinschreiben. Wenn man da mal reinschaut, dann sieht man z.B. eine Funktion “fooBar” und die nimmt ein “x” und dieses “x” ist Number oder String. Das steht dann meistens schon da und es ist nicht in den Sourcen mit drin. Diese Deklarations-Files, die “d.ts”-Files, sind die Formalisierung dieser Doku. Klar muss man aufpassen, dass man es richtig macht. Es kann natürlich auch sein, dass man irgendwelchen Unsinn in den Typen schreibt, weil die Doku vielleicht veraltet ist oder was auch immer. Würde jetzt jemand das Paket installieren und aus TypeScript heraus benutzen, dann sagt der TypeScript-Compiler: Jo, passt alles. Deine Typen stimmen mit der d.ts -Datei überein, aber dann funktioniert es zur Laufzeit aber trotzdem nicht, weil die Deklarationen falsch sind. Das ist in jedem Fall eine mögliche Fehlerquelle.

Lucas Dohmen: Wenn ich jetzt meine Bibliothek wirklich in TypeScript schreibe, kann ich dann auch ein Kompilat erzeugen, was eine JavaScript-Datei ist, plus eine dts-Datei, die aus dem echten TypeScript-Code generiert wurde und dann auch definitiv korrekt ist? Oder geht das nicht?

Lars Hupel: Das ist sogar der Standard-Fall. Wenn man seine Bibliothek direkt in TypeScript schreibt, dann bekommt man standardmäßig zwei verschiedene Dateien heraus: Die JavaScript-Dateien, da kann man z.B. ausfüllen, ob sie CommonJS oder ESM-Module sein sollen. Auf der anderen Seite bekommt man die dts-Files heraus und die sind vom TypeScript-Compiler selbst generiert. Der TypeScript-Compiler trennt die TypeScript-Datei in zwei Teile auf: Der erste Teil ist ohne Typen und der zweite Teil ist nur Typen.

Lucas Dohmen: Es verhält sich also anders, als wenn man eine Anwendng schreibt, denn da braucht man ja die Typdefinition am Ende nicht mehr. Da werden sie einfach herausgenommen, aber wenn ich die Bibliothek baue, dann legt er diese separate Datei an.

Lars Hupel: Genau. Das ist natürlich auch eine Herausforderung an das entsprechende Tooling. Man muss aufpassen, wenn man irgendwelche Packet-Bundler wie “Rollup” oder “Webpack” benutzt, dass alles richtig funktioniert. Das kann man alles irgendwie hinkonfigurieren, aber teilweise ist es ein bisschen holprig, muss ich da zugeben. Wenn man jetzt wirklich eine reine Bibliothek schreibt, dann kann man es einfach nur mit dem TypeScript-Compiler lösen. Man ruft den TSC auf und dann kommt alles richtig heraus, aber sobald man komplexere Sachen machen will, kann es an manchen Stellen manchmal etwas haken.

Lucas Dohmen: Wie ist es denn allgemein in der Praxis? Kommst du gut mit dem TypeScript zurecht oder gibt es da Dinge, die doch noch nicht so gut funktionieren? Ich meine vom Typsystem her, nicht vom Tooling. Passieren da Sachen, die du, als jemand, der vielleicht auch ausgereiftere Typsysteme gewohnt ist, nicht erwarten würdest? Oder ist es generell wirklich gut?

Lars Hupel: In der Praxis funktioniert es erstaunlich gut. Ich kannte TypeScript schon früher, als ich noch nicht mit Web zu tun hatte. Da habe ich mal in irgendeinem Artikel gelesen, dass bestimmte Sachen in TypeScript einfach “unsound” sind, das heißt: Für das Typsystem ist alles okay, aber zur Laufzeit knallt’s. Da wäre in Java eine ClassCastException als Beispiel für so eine “Unsoundness”. In Java kann man also bestimmte Sachen tun, die zur Laufzeit aber knallen. In TypeScript geht das eben auch und da dachte ich: Das ist aber alles Müll! Niemand will es benutzen, wenn es unsound ist! Ein Beispiel für Unsoundness in TypeScript ist, wenn man ein Interface mit bestimmten Methoden hat und man das Interface implementiert. Dann muss man auch schauen, dass die Typen von den Argumenten und Rückgabewerten dieser Methoden übereinstimmen. Damit geht TypeScript etwas “laissez-faire” um. Es gibt ein paar Dinge, die es erlaubt, die aber eigentlich nicht erlaubt sein sollen. Sie begründen es mit Pragmatik, weil es bestimmte JavaScript-Bibliotheken eben so machen und es “blöd” wäre, wenn man sagen würde: Das geht so nicht. Damals dachte ich: Das ist ja voll blöd! Da kann man ja Code schreiben, wo man sich in falscher Sicherheit wähnt. Nachdem ich es jetzt eine Weile benutzt habe, stellt sich aber heraus, dass es echt gar nicht so oft passiert. Ich persönlich bin in Produktion jetzt nur ein Mal in so ein Ding reingelaufen, wenn überhaupt. Ich dache anfangs, es wäre viel schlimmer, als es letztendlich wirklich ist. Eine andere Fehlerquelle, die sein kann, wenn man in das DefinitelyTyped hereinschaut, ist, dass die Typen einfach schlecht sind oder mit der Realität nicht übereinstimmen. Und das ist ein generelles Problem von Open Source. Wenn es nicht richtig ist, dann muss man es eben selbst fixen. Da mache ich auch niemanden einen Vorwuf, denn diese Leute machen es freiwillig in ihrer Freizeit.

Lucas Dohmen: Gerade dort ist die zusätzliche Fehlerquelle, wenn die Bibliothek und die Typdefinition von zwei verschiedenen Gruppen erzeugt werden, dass die Leute, die den Code schreiben, einen anderen Typ zurückgeben. Etwas, was für den “normalen JavaScriptler” eigentlich gar kein großes Ereignis ist, aber dann dazu führt, dass die Typdefinition aktualisiert werden muss. Ich vermute, dass es auch dazu kommen kann, dass schon eine neue Version der Bibliothek draußen ist, aber die neue Version der Typdefinition nicht. Dass es da so einen Abstand geben kann.

Lars Hupel: De facto ist es so, dass alle Sachen, die ich gerade genannt habe, gar keine riesen Probleme sind. In der Praxis funktioniert es wirklich ausgesprochen gut! Viel besser, als ich erst gedacht habe: Oh, da tut man jetzt auch noch so ein Typsystem oben drauf! Darum müssen sich dann noch verschiedene Leute kümmern. Aber doch, es funktioniert doch wirklich erstaunlich gut!

Lucas Dohmen: Eine Sache ist ja die Frage, warum ich so etwas überhaupt tun will? Was denkst du, ist der Vorteil von TypeScript im Gegensatz zu JavaScript?

Lars Hupel: Alle Vorteile, die ein Typsystem mitbringt. Man kann eine ganze Reihe von Fehlern direkt ausschließen und man muss für bestimmte Sachen gar keine Tests mehr schreiben. Ich kann also z.B. darauf vertrauen, dass bei einer Addition, die ich geschrieben habe, nicht irgendwelcher Unfug mit Arrays und Objects passiert. Der zweite Vorteil ist, dass man eine Dokumentation oder zumindest einen Teil davon geschenkt bekommt. Wenn ich eine Library schreibe, die jemand nun benutzen möchte ohne unbedingt den Source Code zu lesen, weil das vielleicht auch überhaupt nicht relevant ist, dann kann man einfach in die Typen hineinschauen und bekommt schon mal eine Basisidee davon, was da überhaupt passiert. Wenn man eine JavaScript-Funktion anschaut, dann hat sie irgendwelche Parameter “foo” und “bar” und dann muss ich raten, was das jetzt sein könnte. Mit React als Beispiel, was ja mittlerweile relativ komplex wird, wenn man sich diese Hooks “useState” und “useEffect” anschaut, dann muss man schon ziemlich aufpassen. UseEffect nimmt, glaube ich, eine Callback-Function, die dann wieder einen Callback zurück gibt, oder so etwas und dabei muss man dann schon ziemlich aufpassen. Dabei ist so ein TypeScript-Compiler super hilfreich, da die gängen Fehler gar nicht erst passieren und man dann nicht erst mehrere Stunden lang Frontend-Tests schreiben muss, die ja sowieso relativ schwierig sind. Stattdessen sind manche Fehlerklassen einfach nicht da und dementsprechend muss man sie auch nicht testen. Am Anfang muss man also Zeit investieren, um die Typen alle richtig hinzubekommen, aber hinterher spart man sich ein Vielfaches davon, weil man keine Tests schreiben muss.

Lucas Dohmen: Wenn du es jetzt mit deiner Erfahung in Scala vergleichst, hast du da dasselbe Level an Sicherheit, dass dir das Typsystem von TypeScript gibt oder siehst da schon noch Unterschiede? Oder gibt dir Scala, als Beispiel, noch mehr an Sicherheit?

Lars Hupel: Ich tue mich mit dem Vergelich ein wenig schwer. In Scala redet man ja meistens mit Scala- oder mit Java-Bibliotheken und Java ist schon getypt. In TypeScript würde ich im Regelfall mit JavaScript- oder TypeScript-Bibliotheken reden, das heißt, die Fehlerquelle kommt dort üblicherweise aus den ungetypten JavaScript-Bibliotheken. Da bekommt man massive Vorteile, denn in JavaScript gibt es ja gar kein Typsystem. In Scala ist es anders, da die Java-Bibliotheken sowieso schon getypt sind und sich da dann nicht so viele Fehler einschleichen können. Scala bietet gegenüber Java noch weitere Abstraktionsmöglichkeiten an. Das Typsystem ist relativ mächtig. In Scala kann man bestimmte Abstraktionen im Code machen, die man in Java nicht machen könnte. In TypeScript ist das anders. In TypeScript kann man nur die gleichen Abstraktionen wie in JavaScript machen. Da kann man sich keinen Code einsparen. Das ist etwas, was das Typsystem von Scala zusätzlich möglich macht: Man kann nicht nur Test-Code sondern auch Applikations-Code einsparen. Das kann man in TypeScript nicht machen, da das Typsystem zwar einserseits mächtig ist, aber andererseits bestimmte Abstraktionen nicht zulässt. Um es mal konkret zu machen: In Scala sind die Generics noch viel stärker aufgebohrt als die in Java. In TypeScript geht das nicht so. Wenn ich z.B. meine Lieblings-Scala-Bibliotheken nach TypeScript portieren wollen würde, hätte ich ziemliche Schwierigkeiten damit.

Lucas Dohmen: Kannst du ein konkretes Beispiel geben, was einem der Scala-Compiler “for free” geben könnte, was ein TypeScript-Compiler nicht geben kann?

Lars Hupel: Darf ich das “M-Wort” sagen?

Lucas Dohmen: Ja, du darfst.

Lars Hupel: Es geht um Monaden. Ich will mich jetzt nicht zu einer Erklärung über Monaden versteigen. Betrachten wir mal die “Promises” aus JavaScript. Promises sind mittlerweile bekannt? Können wir annehmen, oder?

Lucas Dohmen: Du kannst ja ganz kurz sagen, was es ist, aber die meisten haben es schon einmal gehört, glaube ich.

Lars Hupel: Promises sind ein Programmierpattern, mit dem man sagen kann, dass es ein Wert ist, den man erst später bekommt. Zum Beispiel kann man irgendeine Web-Ressource fetchen oder eine Datenbankzugriff machen. Man bekommt dann diesen Wert nicht sofort, sondern irgendwann in der Zukunft und das kann man in einem Promise ausdrücken. Das heißt, man hätte z.B. keine HTTP-Response sondern ein Promise von einer HTTP-Response. Sowohl JavaScript als auch TypeScript bieten Syntax wie “async” und “await” mit dem man es schön hinschreiben kann, anstatt so eine Callback-Hell zu haben. In Scala geht so etwas auch, nur ist es dort viel allgemeiner. Man kann nicht nur über Promises reden, sondern über beliebige Monaden. Da gibt es noch ganz viele andere Typen, die auch “async” und “awaitable” sind und die z.B. mit IO oder anderem zu tun haben. In Scala kann man es sehr uniform ausdrücken und in TypeScript geht es nur mit Promises. Wenn man jetzt eine super Idee für Premium-Promises, die noch irgendwelche anderen Sachen könnnen, hat, dann kann man so etwas nicht in TypeScript machen. In TypeScript sind “asnyc” und “await” festgenagelt auf Promises.

Lucas Dohmen: Das habe ich verstanden. Das heißt, für die meisten Fälle reicht das, was das Typsystem kann, schon aus?

Lars Hupel: Das kommt darauf an, wie du “die meisten” definierst. Wenn ich zum Beispiel nicht aus einer Scala- oder Haskell-Welt kommen würde, mir Monaden völlig fremd wären, dann würde ich wahrscheinlich auch nicht sehen, wofür ich es brauche könnte. Ich habe jetzt schon früher sehr viel Code in Scala und Haskell geschrieben und vermisse es ein bisschen. Es kommt also auch darauf an, welchen Erfahrungschatz man hat. Leute, die aus JavaScript kommen, die kennen “async” und “await” mit Promises und freuen sich darüber, dass es in TypeScript auch geht. Für diese Leute ist es sicherlich völlig akzeptabel. Mich schmerzt es ein wenig, weil ich auch gesehen habe, was man machen kann, wenn man noch mehr Asbtraktion benutzen kann. Ich sehe aber auch ein, dass Abstraktion auch immer dazu führt, dass man sich mehr Gedanken machen muss, wie man das Programm ließt. Es ist also nicht unbedingt kostenlos.

Lucas Dohmen: Vor allem muss da im Hinterkopf behalten werden, dass das Design-Problem, das TypeScript löst, ein anderes ist. Denn sie wollen ja trotzdem immer noch gucken, dass es mit dem ganzen JavaScript-Ökosystem zusammen funktioniert und das schränkt es automatisch schon ein.

Lars Hupel: Genau, auf jeden Fall. Ich will auch niemandem einen Strick daraus drehen und behaupten, es sei unbenutzbar dadurch. Es ist etwas, wo ich sage, dass es cool wäre, wenn sie es hätten. Es funktioniert ja trotzdem sehr gut, wie ich schon mehrmals gesagt habe.

Lucas Dohmen: Genau, cool! Hast du denn sonst noch Beispiele von dir oder von anderen Quellen, wo jemandem die Umstellung auf TypeScript geholfen hat?

Lars Hupel: Vor wenigen Wochen gab es ein paar Blogposts von Gary Bernhardt, der früher, glaube ich, einmal Pyhthonista und dann Rubyist war, und nun ist er zum TypeScripty geworden. Er hat eine Webseite, auf der man Programmierübungen oder programmieren lernen kann. Das ganze Frontend war in React und das Backend in Ruby. Dann haben sie es alles komplett in TypeScript neu geschrieben, also sowohl Frontend als auch Backend. Daraufhin haben sie nun ein paar Blogposts darüber geschrieben, welche Fehlerquellen sie sich dadurch komplett ersparen. In React ist es z.B. so, dass man sehr viel mit “Props” arbeitet, also Komponenten. Diese Komponenten haben Attribute und da passiert es ganz oft, dass man dort die falschen Attribute übergibt. Es ist sehr einfach falsch zu machen. Mit TypeScript passiert das einfach nicht mehr. Diese Fehlerklasse ist komplett ausgeschlossen. Oder dadurch, dass sie ein Frontend und Backend haben, müssen sie auch Routen bauen, sodass man sagen kann, was passiert, wenn auf eine Ressource zugegriffen wird. Beides braucht man sowohl auf dem Client als auch auf dem Server. Sie haben es jetzt typsicher gemacht, d.h. man kann gar keine Routen mehr bauen, die falsch sind, und das finde ich auch sehr cool. Sie haben noch etwas Drittes angesprochen, nämlich ein Tool, mit dem man aus ihrer SQL-Datenbank im Backend die entsprechenden TypeScript-Definition herausbekommt. Das führt dazu, dass die SQL-Queries immer korrekt getypt sind. Man macht z.B. irgendein “SELECT” auf irgendetwas und bekommt immer den richtige Typ raus, ohne immer zu schauen, ob in “Spalte fünf” jetzt ein Number oder ein String ist. In so gut wie allen Sprachen ist das nämlich ein Problem, selbst wenn man eine geytpte Sprache hat. Für mich persönlich nehme ich daraus mit – und ich kenne diese Fehlerquellen auch alle – dass der größte Vorteil ist: Das Refactoring wird viel, viel entspannter. Man muss nicht erst schauen, dass man 99 Prozent Test-Coverage hat, bevor man mit dem Refactoring anfangen kann. Wenn man irgendein Problem sieht, dann refactort man, und wenn’s kompiliert, dann funktioniert’s auch. Das ist ein riesiger Vorteil und das kann mir eine Sprache wie JavaScript nicht geben. Da mus sich eben gucken, dass man möglichst viel Test-Coverage hat. Erst dann bin ich mir sicher, dass mein Refactoring funktioniert hat.

Lucas Dohmen: Bedeutet es auch, dass es da Tooling-Support gibt? Zum Beispiel das Renaming von Sachen in einer sicheren Art und Weise? Also diese typischen, einfachen Refactorings auch automatisch durchzuführen. Ist das auch etwas, was da geht?

Lars Hupel: Ich mache es mit IntelliJ, die haben ein sehr gutes TypeScript-Plug-In. Es funktioniert so, wie man es für andere Programmiersprachen auch kennt. Man hat Code-Completion, Find-Usage und so weiter. Und das alles funktioniert sehr zuverlässig, weil sie Zugriff auf die Typen haben. Fairerweise muss man dazu sagen, dass IntelliJ einen guten JavaScript-Support hat, aber da fehlt eben zusätzliche Information. Da kann man diese Refactorings nicht so problemlos durchführen.

Lucas Dohmen: Also sowohl toolgestütztes als auch manuelles Refactoring fällt dir einfach viel leichter.

Lars Hupel: Genau und ich möchte behaupten, dass es sicherlich auch für VSCode und die ganzen anderen Editoren und IDEs Plug-Ins gibt. Dadurch dass Microsoft dahinter steht, haben sie da natürlich entsprechend Möglichkeiten und Ressourcen, um gutes Tooling zu bauen.

Lucas Dohmen: Cool! Gibt es sonst noch Beispiele, die du uns mitgeben willst?

Lars Hupel: Ja, ein Beispiel aus einem aktuellen Projekt. Man kann da jetzt diskutieren, ob es eine coole Sache ist oder ob ich mich da etwas verkünstelt habe: Ich habe da eine Bibliothek für “Typesafe Remote-Procedure-Calls” geschrieben. Ja, ich habe quasi SOAP neu erfunden.

Lucas Dohmen: Wurde mal wieder Zeit!

Lars Hupel: Ja, nee. Da geht es eigentlich darum, dass man mit iFrames kommuniziert. Es gibt also Code, der innerhalb des iFrames läuft und Code, der außerhalb läuft. Zwischen diesen zwei Domänen kann man in JavaScript mit einem sogenannten “postMessage” kommunizieren und Objekte hin- und herschicken. Das Problem ist, dass man nur Objekte hin- und herschicken, aber keine Methode direkt aufrufen kann. Das ist aus Sicherheitsgründen auch so gewollt. Man will nicht, dass der iFrame beliebigen Kram außenrum manipulieren will, denn dafür ist der iFrame gedacht: Er isoliert Code von der Außenwelt oder die Außenwelt vom Code, also beidseitig. Man kann über dieses postMessage-Objekte hin-und herschicken und ich fand es super aufwendig, es zu machen. Man baut sich irgendwelche Objekte zusammen und an denen stehen dann vielleicht Label dran oder irgendein tag oder was auch immer. Dann muss man sich irgendwelche Argumente zusammendröseln. Vielleicht hat man auch geschachtelte Funkionsaufrufe. Ich habe mir dann gedacht, dass es doch irgendwie cooler gehen muss. Man kann es jetzt so machen, dass man in TypeScript ein Interface definiert. Dieses Interface ist das Objekt, dass man letztendlich aufrufen will. Zum Beispiel Zugriffe auf Local-Storage oder sonst irgendwelche anderen Sachen. Das ist ein ganz normales Objekt. Auf Clientseite kann man dieses Objekt nun aufrufen und die Bibliothek übernimmt die Übersetzung in diesen postMessage-Call. Auf der anderen Seite übernimmt die Bibliothek die Übersetzung von dem postMessage-Call wieder zurück in den eigentlichen Methodenaufruf. Umgekehrt eben auch. Das Gleiche funktioniert für verschachtelete Aufrufe und da können auch Promises involviert sein. Das geht alles super. Das Coole daran ist – es gibt eigentlich zwei coole Sachen: Wenn der Client versucht, eine Methode aufzurufen, die es nicht gibt, dann sagt der Compiler: Nö, darfst du nicht! Der zweite coole Punkt ist auf Serverseite: Wenn der Server diese API implementieren will, also sagen muss, was passiert, wenn diese Methode aufgerufen wird und der Server sie nun falsch implementiert, dann gibt es auch einen Fehler von TypeScript. Man hat also sowohl den Client als auch den Server getypt und typgecheckt und beide sind aus dem gleichen Interface heraus generiert. Ich schreibe also einmal mein ideales Interface, das ich haben will, und dann kann mir die Bibliothek einen Client und einen Server daraus erzeugen. Ich habe dann mal versucht, wie weit man es treiben kann und festgestellt, dass es eigentlich völlig irrelevant ist, dass diese Sachen über postMessage in iFrames kommunizieren. Man kann das Ganze auch über HTTP legen oder über andere Transportmöglichkeiten. Der Client- und der Servercode sind fast identisch. Es gibt keine großen Unterschiede. Insbesondere müssen weder Client noch Server wissen, worüber sie kommunizieren. Alles ist typgecheckt und wunderbar “unicorns and rainbows”.

Lucas Dohmen: Der SOAP-Traum lebt!

Lars Hupel: Ich würde jetzt nicht behaupten wollen, dass man es unbedingt im HTTP sprechen will. Für Testzwecke ist es manchmal ganz praktisch, wenn man es über andere Transportwege testen kann.

Lucas Dohmen: Ja, speziell auch für diesen Use-Case, an dem du auch arbeitest. Cool! Vielen Dank, Lars, für die gute Übersicht! Wenn du sonst nichts mehr hast?

Lars Hupel: Nö, ich glaube, ich habe jetzt alle hinreichend missioniert.

Lucas Dohmen: Okay, wunderbar! Dann sage ich den Hörerinnen und Hörern: Bis zum nächsten Mal!

Lars Hupel: Danke!

Alumnus

Lucas was a senior consultant at INNOQ until August 2023. He works on the architecture, conception, and implementation of web applications on the front and back end. He programs in Ruby and JavaScript and helps with technology decisions & the adoption of different NoSQL solutions. Lucas is one of the authors of the book “The Rails 7 Way”. You can hear his voice on the INNOQ podcast quite regularly. He does open source and community work (like organizing and teaching at the local CoderDojo).

Alumnus

Lars worked as Senior Consultant with INNOQ in Munich until December 2022. They are interested in programming languages – especially the functional variety –, web development, and theoretical computer science. They write articles and talk about a multitude of topics.