Aus JavaScript mach‘ TypeScript – so geht’s.Joyseulay | shutterstock.com TypeScript ist eine stark typisierte Variante von JavaScript – und ein performantes Tool für Entwickler, um Bugs auf ein Minimum zu reduzieren und JavaScript-Programme schmerzfrei(er) in Enterprise-Umgebungen zu erstellen. Dabei läuft TypeScript überall, wo auch JavaScript läuft und kompiliert auch selbst zu JavaScript. Und: Alle vorhandenen JavaScript-Programme sind bereits gültiges TypeScript – nur eben ohne die von TypeScript bereitgestellten Typinformationen. Soll heißen: Sie können bestehende JavaScript-Programme in TypeScript umwandeln – und zwar schrittweise. In diesem Tutorial lesen Sie, wie das geht. TypeScript-Compiler einrichten TypeScript ist ein eigenständiges Projekt, das nichts mit JavaScript zu tun hat. Insofern ist es erforderlich, den TypeScript-Compiler zu installieren. Außerdem benötigen Sie Node.js und npm: npm install -g typescript Sie können auch andere Projekte im JavaScript-Ökosystem nutzen, um mit TypeScript zu arbeiten: Bun bündelt beispielsweise den TypeScript-Compiler automatisch, so dass Sie nichts weiter installieren müssen. Und auch die Deno-Runtime bietet integrierten Support für TypeScript. Wenn Sie ohnehin schon mit dem Gedanken spielen, auf eines dieser Projekte umzusteigen, warum nicht gleich mit TypeScript? TypeScript in JavaScript kompilieren Den TypeScript-Compiler einfach auf bestehenden JavaScript-Code „loszulassen“ ist der einfachste Weg, diesen in TypeScript zu kompilieren: tsc myfile.ts TypeScript-Dateien verwenden die Dateiendung .ts. Wenn sie durch den Compiler laufen, werden sie in .js-Dateien umgewandelt – mit gleichem Namen am gleichen Platz. Für einzelne Dateien ist das in Ordnung. Wahrscheinlicher ist jedoch, dass Sie ein ganzes Projektverzeichnis mit diversen Dateien kompilieren wollen. Um das ohne großen Aufwand zu tun, müssen Sie eine simple Konfigurationsdatei für Ihr Projekt schreiben. Die TypeScript-Konfigurationsdatei heißt in aller Regel tsconfig.json und befindet sich im Stammverzeichnis Ihres Projekts. Eine einfache Variante könnte folgendermaßen aussehen: { “compilerOptions”: { “outDir”: “./jssrc”, “allowJs”: true, “target”: “es6”, “sourceMap”: true }, “include”: [“./src/**/*”] } In diesem Code-Snippet teilt compilerOptions dem Compiler mit, wo eine kompilierte Datei abgelegt werden soll. „outDir“: „./jssrc“ bedeutet, dass alle generierten .js-Dateien in einem Verzeichnis namens jssrc („JavaScript Source“) abgelegt werden. Sie können aber auch einen beliebigen Namen verwenden, der zu Ihrem Projektlayout passt. Außerdem wird mit „allowJs“: true festgelegt, dass reguläre JavaScript-Dateien als Input akzeptiert werden. Das gewährleistet, dass Sie JavaScript- und TypeScript-Dateien problemlos in Ihrem src-Ordner mischen können. Wenn Sie outDir nicht spezifizieren, werden die JavaScript-Dateien neben den entsprechenden TypeScript-Dateien im Quellverzeichnis abgelegt. Das ist unter Umständen ungünstig, etwa wenn Sie die generierten Dateien zu Debugging-Zwecken in einem separaten Verzeichnis ablegen möchten. In unserer Konfigurationsdatei können wir auch definieren, nach welchem ECMAScript-Standard kompiliert werden soll. „target“: „es6“ bedeutet, dass wir ECMAScript 6 (ES6) verwenden. Die meisten JavaScript-Engines und Browser unterstützen mittlerweile ES6 – das ist also eine akzeptable Standardeinstellung. „sourceMap“: true zu spezifizieren, generiert .js.map-Dateien zusammen mit allen generierten JavaScript-Dateien für die Fehlersuche. Last, but not least stellt der include-Abschnitt ein glob-Pattern zur Verfügung, um zu verarbeitende Quelldateien zu finden. Darüber hinaus bietet tsconfig.json zahlreiche weitere Optionen, aber die genannten sollten für den Einstieg völlig ausreichen. Nachdem Sie tsconfig.json eingerichtet haben, können Sie tsc im Stammverzeichnis des Projekts ausführen und Dateien in dem durch outDir spezifizierten Verzeichnis generieren. Dabei sollten Sie darauf achten, ein outDir zu spezifizieren, das nicht mit anderen Dateien in Konflikt steht. Typ-Annotationen hinzufügen Im nächsten Schritt gilt es, den existierenden JavaScript-Code schrittweise zu TypeScript zu migrieren. Da alle bestehenden JavaScript-Dateien bereits gültiges TypeScript sind, eine Datei nach der anderen bearbeiten, indem Sie bestehende .js– in .ts-Dateien umbenennen. Solange eine JavaScript-Datei keine Typ-Informationen oder andere TypeScript-spezifische Syntax enthält, wird der Compiler nicht tätig. Mit Blick auf TypScript-Annotationen ist ein möglicher Startpunkt, sie zu Funktionssignaturen und Rückgabetypen hinzuzufügen. Im Folgenden eine JavaScript-Funktion ohne Typ-Annotationen. Sie dient dazu, den Namen einer Person im Nachname-Vorname-Format zu generieren, basierend auf einem Objekt mit den Eigenschaften .firstName und .lastName. function lastNameFirst(person) { return `${person.lastName}, ${person.firstName}`; } Mit TypeScript lässt sich deutlich expliziter gestalten, was akzeptiert und zurückgegeben wird. Wir stellen lediglich Typ-Annotationen für die Argumente und den Rückgabewert zur Verfügung: function lastNameFirst(person: Person): string { return `${person.lastName}, ${person.firstName}`; } Dieser Code geht davon aus, dass wir zuvor im Code einen Objekttyp namens Person definiert haben. Außerdem nutzt er string sowohl in JavaScript als auch in TypeScript als integrierten Typ. Indem wir diese Annotationen hinzufügen, stellen wir sicher, dass jeder Code, der diese Funktion aufruft, ein Objekt vom Typ Person bereitstellen muss. Wird stattdessen DogBreed bereitstellt, gibt der Compiler folgende Fehlermeldung aus: error TS2345: Argument of type ‘DogBreed’ is not assignable to parameter of type ‘Person’. Type ‘DogBreed’ is missing the following properties from type ‘Person’: firstName, lastName Mit den Fehlerdetails erhalten Sie nicht nur eine Warnung, dass es sich nicht um den richtigen Typ handelt: Sie bekommen auch Hinweise dazu, warum dieser Typ für eine bestimmte Instanz nicht funktioniert. Das liefert aber nicht einen Anhaltspunkt darüber, wie das unmittelbare Problem behoben werden kann. Es regt zudem dazu an, darüber nachzudenken, wie Typen erweitert oder eingeschränkt werden können, um sich an Use Cases anzupassen. Interface-Deklarationen Eine Interface Declaration bietet eine weitere Möglichkeit, zu beschreiben, welche Typen mit etwas verwendet werden können. Ein Interface (Schnittstelle) ermöglicht es, zu beschreiben, was erwartet werden kann, ohne das vollständig definieren zu müssen. Ein Beispiel: interface Name { firstName: string; lastName: string; } function lastNameFirst(person: Name): string { return `${person.lastName}, ${person.firstName}`; } In einem Fall wie diesem könnten wir jeden beliebigen Typ an lastnameFirst() übergeben, solange er die Eigenschaften .firstName und .lastName aufweist und es sich dabei um string-Typen handelt. So können Sie Typen erstellen, die sich auf die Form des verwendeten Objekts beziehen und nicht darauf, ob es sich um einen bestimmten Typ handelt. TypeScript-Typen identifizieren Wenn Sie JavaScript-Code mit Annotationen versehen, um TypeScript zu erstellen, werden Ihnen die meisten verwendeten Typinformationen bekannt sein, da sie aus JavaScript-Typen stammen. Wie und wo Sie diese Typen anwenden, muss jedoch gut überlegt sein. Im Allgemeinen müssen Sie keine Annotationen für Literale hinzufügen, weil die automatisch abgeleitet werden können. Beispielsweise ist name: string = „Davis“; redundant, da aus der Zuweisung zum Literal klar hervorgeht, dass name ein string ist. Bei vielen anonymen Funktionen können die Typen ebenfalls auf diese Weise abgeleitet werden. Primitive Typen – string, number und boolean – können auf Variablen angewendet werden, die diese Typen verwenden und bei denen sie nicht automatisch abgeleitet werden können. Für Arrays von Typen können Sie den Typ gefolgt von [] (beispielsweise number[] für ein Array von Zahlen) oder die Syntax Array verwenden (in diesem Fall Array). Eigene Typen definieren Sie mit dem Keyword type: type FullName = { firstName: string; lastName: string; }; Das ließe sich auch nutzen, um ein Objekt zu erstellen, das seiner Typform entspricht: var myname:FullName = {firstName:”Brad”, lastName:”Davis”}; Allerdings würde das in einem Fehler resultieren: var myname:FullName = {firstName:”Brad”, lastName:”Davis”, middleName:”S.”}; Der Grund: middleName ist in unserem Typ nicht definiert. Mit dem Operator | können Sie angeben, dass mehrere, verschiedene Typen möglich sind: type userName = Fullname | string; // or we can use it in a function signature … function doSomethingWithName(name: Fullname|string) {…} Wenn Sie einen neuen Typ erstellen möchten, der eine aus bestehenden Typen zusammengesetzt ist (ein Intersection-Typ) nutzen Sie dazu den Operator &: type Person = { firstName: string; lastName: string; }; type Bibliography = { books: Array; }; type Author = Person & Bibliography; // we can then create an object that uses fields from both types: var a: Author = { firstName: “Serdar”, lastName: “Yegulalp”, books: [“Python Made Easy”, “Python Made Complicated”] }; Zu beachten ist dabei, dass Sie zwar mit Typen so umgehen können – wenn Sie jedoch etwas Ähnliches mit Interfaces umsetzen wollen, müssen Sie einen anderen Ansatz verwenden. Der führt über das Keyword extends: interface Person { firstName: string; lastName: string; } interface Author extends Person { penName: string; } JavaScript-Klassen werden ebenfalls als Typen berücksichtigt. Mit TypeScript können Sie sie unverändert mit Typ-Annotationen verwenden: class Person { name: string; constructor( public firstName: string, public lastName: string ) { this.firstName = firstName; this.lastName = lastName; this.name = `${firstName} ${lastName}`; } } TypeScript verfügt außerdem auch über einige spezielle Typen für andere Fälle. any wird seinem Namen gerecht: Jeder Typ wird akzeptiert. null und undefined haben dieselbe Bedeutung wie in normalem JavaScript (beispielsweise würden Sie string|null verwenden, um einen Typ anzugeben, der entweder eine Zeichenfolge oder ein null-Wert ist. TypeScript unterstützt auch nativ den Postfix-Operator !. So stellt etwa x!.action() sicher, dass .action() auf x aufgerufen wird – solange x nicht null oder undefined ist. Wenn Sie auf eine Funktion verweisen möchten, die eine bestimmte Form von Type Expression nutzt, können Sie dazu eine sogenannte „Call-Signatur“ verwenden: function runFn(fn: (arg: number) => any, value: number): any { return fn(value); } runFn würde eine Funktion akzeptieren, die eine einzelne Zahl als Argument nimmt und einen beliebigen Wert zurückgibt. Beachten Sie, dass wir hier die Pfeilnotation verwenden, um anzugeben, was die übergebene Funktion zurückgibt, nicht einen Doppelpunkt, wie wir es in der Signatur der Hauptfunktion tun. TypeScript-Projekt aufbauen Viele Build-Tools im JavaScript-Ökosystem sind mittlerweile TypeScript-fähig: Die Frameworks tsdx, Angular und Nest können eine JavaScript-Codebasis mit wenig Aufwand automatisch in den passenden TypeScript-Code umwandeln. Wenn Sie mit einem Build-Tool wie Babel, webpack oder anderen arbeiten, können auch diese TypeScript-Projekte verarbeiten, insofern Sie TypeScript-Handling als Extension installieren oder manuell aktivieren. Um bestehende JavaScript-Projekte erfolgreich auf TypeScript umzustellen, empfiehlt es sich in erster Linie, Schritt für Schritt vorzugehen: Migrieren Sie jeweils ein Modul und dann eine Funktion. TypeScript und JavaScript können koexistieren – Sie müssen also nicht alles auf einmal migrieren. Nehmen Sie sich die Zeit, zu experimentieren und die besten Typen für Ihre Codebasis zu finden. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!
Tutorial: Von JavaScript zu TypeScript
Aus JavaScript mach‘ TypeScript – so geht’s.Joyseulay | shutterstock.com TypeScript ist eine stark typisierte Variante von JavaScript – und ein performantes Tool für Entwickler, um Bugs auf ein Minimum zu reduzieren und JavaScript-Programme schmerzfrei(er) in Enterprise-Umgebungen zu erstellen. Dabei läuft TypeScript überall, wo auch JavaScript läuft und kompiliert auch selbst zu JavaScript. Und: Alle vorhandenen JavaScript-Programme sind bereits gültiges TypeScript – nur eben ohne die von TypeScript bereitgestellten Typinformationen. Soll heißen: Sie können bestehende JavaScript-Programme in TypeScript umwandeln – und zwar schrittweise. In diesem Tutorial lesen Sie, wie das geht. TypeScript-Compiler einrichten TypeScript ist ein eigenständiges Projekt, das nichts mit JavaScript zu tun hat. Insofern ist es erforderlich, den TypeScript-Compiler zu installieren. Außerdem benötigen Sie Node.js und npm: npm install -g typescript Sie können auch andere Projekte im JavaScript-Ökosystem nutzen, um mit TypeScript zu arbeiten: Bun bündelt beispielsweise den TypeScript-Compiler automatisch, so dass Sie nichts weiter installieren müssen. Und auch die Deno-Runtime bietet integrierten Support für TypeScript. Wenn Sie ohnehin schon mit dem Gedanken spielen, auf eines dieser Projekte umzusteigen, warum nicht gleich mit TypeScript? TypeScript in JavaScript kompilieren Den TypeScript-Compiler einfach auf bestehenden JavaScript-Code „loszulassen“ ist der einfachste Weg, diesen in TypeScript zu kompilieren: tsc myfile.ts TypeScript-Dateien verwenden die Dateiendung .ts. Wenn sie durch den Compiler laufen, werden sie in .js-Dateien umgewandelt – mit gleichem Namen am gleichen Platz. Für einzelne Dateien ist das in Ordnung. Wahrscheinlicher ist jedoch, dass Sie ein ganzes Projektverzeichnis mit diversen Dateien kompilieren wollen. Um das ohne großen Aufwand zu tun, müssen Sie eine simple Konfigurationsdatei für Ihr Projekt schreiben. Die TypeScript-Konfigurationsdatei heißt in aller Regel tsconfig.json und befindet sich im Stammverzeichnis Ihres Projekts. Eine einfache Variante könnte folgendermaßen aussehen: { "compilerOptions": { "outDir": "./jssrc", "allowJs": true, "target": "es6", "sourceMap": true }, "include": ["./src/**/*"] } In diesem Code-Snippet teilt compilerOptions dem Compiler mit, wo eine kompilierte Datei abgelegt werden soll. „outDir“: „./jssrc“ bedeutet, dass alle generierten .js-Dateien in einem Verzeichnis namens jssrc („JavaScript Source“) abgelegt werden. Sie können aber auch einen beliebigen Namen verwenden, der zu Ihrem Projektlayout passt. Außerdem wird mit „allowJs“: true festgelegt, dass reguläre JavaScript-Dateien als Input akzeptiert werden. Das gewährleistet, dass Sie JavaScript- und TypeScript-Dateien problemlos in Ihrem src-Ordner mischen können. Wenn Sie outDir nicht spezifizieren, werden die JavaScript-Dateien neben den entsprechenden TypeScript-Dateien im Quellverzeichnis abgelegt. Das ist unter Umständen ungünstig, etwa wenn Sie die generierten Dateien zu Debugging-Zwecken in einem separaten Verzeichnis ablegen möchten. In unserer Konfigurationsdatei können wir auch definieren, nach welchem ECMAScript-Standard kompiliert werden soll. „target“: „es6“ bedeutet, dass wir ECMAScript 6 (ES6) verwenden. Die meisten JavaScript-Engines und Browser unterstützen mittlerweile ES6 – das ist also eine akzeptable Standardeinstellung. „sourceMap“: true zu spezifizieren, generiert .js.map-Dateien zusammen mit allen generierten JavaScript-Dateien für die Fehlersuche. Last, but not least stellt der include-Abschnitt ein glob-Pattern zur Verfügung, um zu verarbeitende Quelldateien zu finden. Darüber hinaus bietet tsconfig.json zahlreiche weitere Optionen, aber die genannten sollten für den Einstieg völlig ausreichen. Nachdem Sie tsconfig.json eingerichtet haben, können Sie tsc im Stammverzeichnis des Projekts ausführen und Dateien in dem durch outDir spezifizierten Verzeichnis generieren. Dabei sollten Sie darauf achten, ein outDir zu spezifizieren, das nicht mit anderen Dateien in Konflikt steht. Typ-Annotationen hinzufügen Im nächsten Schritt gilt es, den existierenden JavaScript-Code schrittweise zu TypeScript zu migrieren. Da alle bestehenden JavaScript-Dateien bereits gültiges TypeScript sind, eine Datei nach der anderen bearbeiten, indem Sie bestehende .js– in .ts-Dateien umbenennen. Solange eine JavaScript-Datei keine Typ-Informationen oder andere TypeScript-spezifische Syntax enthält, wird der Compiler nicht tätig. Mit Blick auf TypScript-Annotationen ist ein möglicher Startpunkt, sie zu Funktionssignaturen und Rückgabetypen hinzuzufügen. Im Folgenden eine JavaScript-Funktion ohne Typ-Annotationen. Sie dient dazu, den Namen einer Person im Nachname-Vorname-Format zu generieren, basierend auf einem Objekt mit den Eigenschaften .firstName und .lastName. function lastNameFirst(person) { return `${person.lastName}, ${person.firstName}`; } Mit TypeScript lässt sich deutlich expliziter gestalten, was akzeptiert und zurückgegeben wird. Wir stellen lediglich Typ-Annotationen für die Argumente und den Rückgabewert zur Verfügung: function lastNameFirst(person: Person): string { return `${person.lastName}, ${person.firstName}`; } Dieser Code geht davon aus, dass wir zuvor im Code einen Objekttyp namens Person definiert haben. Außerdem nutzt er string sowohl in JavaScript als auch in TypeScript als integrierten Typ. Indem wir diese Annotationen hinzufügen, stellen wir sicher, dass jeder Code, der diese Funktion aufruft, ein Objekt vom Typ Person bereitstellen muss. Wird stattdessen DogBreed bereitstellt, gibt der Compiler folgende Fehlermeldung aus: error TS2345: Argument of type 'DogBreed' is not assignable to parameter of type 'Person'. Type 'DogBreed' is missing the following properties from type 'Person': firstName, lastName Mit den Fehlerdetails erhalten Sie nicht nur eine Warnung, dass es sich nicht um den richtigen Typ handelt: Sie bekommen auch Hinweise dazu, warum dieser Typ für eine bestimmte Instanz nicht funktioniert. Das liefert aber nicht einen Anhaltspunkt darüber, wie das unmittelbare Problem behoben werden kann. Es regt zudem dazu an, darüber nachzudenken, wie Typen erweitert oder eingeschränkt werden können, um sich an Use Cases anzupassen. Interface-Deklarationen Eine Interface Declaration bietet eine weitere Möglichkeit, zu beschreiben, welche Typen mit etwas verwendet werden können. Ein Interface (Schnittstelle) ermöglicht es, zu beschreiben, was erwartet werden kann, ohne das vollständig definieren zu müssen. Ein Beispiel: interface Name { firstName: string; lastName: string; } function lastNameFirst(person: Name): string { return `${person.lastName}, ${person.firstName}`; } In einem Fall wie diesem könnten wir jeden beliebigen Typ an lastnameFirst() übergeben, solange er die Eigenschaften .firstName und .lastName aufweist und es sich dabei um string-Typen handelt. So können Sie Typen erstellen, die sich auf die Form des verwendeten Objekts beziehen und nicht darauf, ob es sich um einen bestimmten Typ handelt. TypeScript-Typen identifizieren Wenn Sie JavaScript-Code mit Annotationen versehen, um TypeScript zu erstellen, werden Ihnen die meisten verwendeten Typinformationen bekannt sein, da sie aus JavaScript-Typen stammen. Wie und wo Sie diese Typen anwenden, muss jedoch gut überlegt sein. Im Allgemeinen müssen Sie keine Annotationen für Literale hinzufügen, weil die automatisch abgeleitet werden können. Beispielsweise ist name: string = „Davis“; redundant, da aus der Zuweisung zum Literal klar hervorgeht, dass name ein string ist. Bei vielen anonymen Funktionen können die Typen ebenfalls auf diese Weise abgeleitet werden. Primitive Typen – string, number und boolean – können auf Variablen angewendet werden, die diese Typen verwenden und bei denen sie nicht automatisch abgeleitet werden können. Für Arrays von Typen können Sie den Typ gefolgt von [] (beispielsweise number[] für ein Array von Zahlen) oder die Syntax Array verwenden (in diesem Fall Array). Eigene Typen definieren Sie mit dem Keyword type: type FullName = { firstName: string; lastName: string; }; Das ließe sich auch nutzen, um ein Objekt zu erstellen, das seiner Typform entspricht: var myname:FullName = {firstName:"Brad", lastName:"Davis"}; Allerdings würde das in einem Fehler resultieren: var myname:FullName = {firstName:"Brad", lastName:"Davis", middleName:"S."}; Der Grund: middleName ist in unserem Typ nicht definiert. Mit dem Operator | können Sie angeben, dass mehrere, verschiedene Typen möglich sind: type userName = Fullname | string; // or we can use it in a function signature ... function doSomethingWithName(name: Fullname|string) {...} Wenn Sie einen neuen Typ erstellen möchten, der eine aus bestehenden Typen zusammengesetzt ist (ein Intersection-Typ) nutzen Sie dazu den Operator &: type Person = { firstName: string; lastName: string; }; type Bibliography = { books: Array; }; type Author = Person & Bibliography; // we can then create an object that uses fields from both types: var a: Author = { firstName: "Serdar", lastName: "Yegulalp", books: ["Python Made Easy", "Python Made Complicated"] }; Zu beachten ist dabei, dass Sie zwar mit Typen so umgehen können – wenn Sie jedoch etwas Ähnliches mit Interfaces umsetzen wollen, müssen Sie einen anderen Ansatz verwenden. Der führt über das Keyword extends: interface Person { firstName: string; lastName: string; } interface Author extends Person { penName: string; } JavaScript-Klassen werden ebenfalls als Typen berücksichtigt. Mit TypeScript können Sie sie unverändert mit Typ-Annotationen verwenden: class Person { name: string; constructor( public firstName: string, public lastName: string ) { this.firstName = firstName; this.lastName = lastName; this.name = `${firstName} ${lastName}`; } } TypeScript verfügt außerdem auch über einige spezielle Typen für andere Fälle. any wird seinem Namen gerecht: Jeder Typ wird akzeptiert. null und undefined haben dieselbe Bedeutung wie in normalem JavaScript (beispielsweise würden Sie string|null verwenden, um einen Typ anzugeben, der entweder eine Zeichenfolge oder ein null-Wert ist. TypeScript unterstützt auch nativ den Postfix-Operator !. So stellt etwa x!.action() sicher, dass .action() auf x aufgerufen wird – solange x nicht null oder undefined ist. Wenn Sie auf eine Funktion verweisen möchten, die eine bestimmte Form von Type Expression nutzt, können Sie dazu eine sogenannte „Call-Signatur“ verwenden: function runFn(fn: (arg: number) => any, value: number): any { return fn(value); } runFn würde eine Funktion akzeptieren, die eine einzelne Zahl als Argument nimmt und einen beliebigen Wert zurückgibt. Beachten Sie, dass wir hier die Pfeilnotation verwenden, um anzugeben, was die übergebene Funktion zurückgibt, nicht einen Doppelpunkt, wie wir es in der Signatur der Hauptfunktion tun. TypeScript-Projekt aufbauen Viele Build-Tools im JavaScript-Ökosystem sind mittlerweile TypeScript-fähig: Die Frameworks tsdx, Angular und Nest können eine JavaScript-Codebasis mit wenig Aufwand automatisch in den passenden TypeScript-Code umwandeln. Wenn Sie mit einem Build-Tool wie Babel, webpack oder anderen arbeiten, können auch diese TypeScript-Projekte verarbeiten, insofern Sie TypeScript-Handling als Extension installieren oder manuell aktivieren. Um bestehende JavaScript-Projekte erfolgreich auf TypeScript umzustellen, empfiehlt es sich in erster Linie, Schritt für Schritt vorzugehen: Migrieren Sie jeweils ein Modul und dann eine Funktion. TypeScript und JavaScript können koexistieren – Sie müssen also nicht alles auf einmal migrieren. Nehmen Sie sich die Zeit, zu experimentieren und die besten Typen für Ihre Codebasis zu finden. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!
Tutorial: Von JavaScript zu TypeScript Aus JavaScript mach‘ TypeScript – so geht’s.Joyseulay | shutterstock.com TypeScript ist eine stark typisierte Variante von JavaScript – und ein performantes Tool für Entwickler, um Bugs auf ein Minimum zu reduzieren und JavaScript-Programme schmerzfrei(er) in Enterprise-Umgebungen zu erstellen. Dabei läuft TypeScript überall, wo auch JavaScript läuft und kompiliert auch selbst zu JavaScript. Und: Alle vorhandenen JavaScript-Programme sind bereits gültiges TypeScript – nur eben ohne die von TypeScript bereitgestellten Typinformationen. Soll heißen: Sie können bestehende JavaScript-Programme in TypeScript umwandeln – und zwar schrittweise. In diesem Tutorial lesen Sie, wie das geht. TypeScript-Compiler einrichten TypeScript ist ein eigenständiges Projekt, das nichts mit JavaScript zu tun hat. Insofern ist es erforderlich, den TypeScript-Compiler zu installieren. Außerdem benötigen Sie Node.js und npm: npm install -g typescript Sie können auch andere Projekte im JavaScript-Ökosystem nutzen, um mit TypeScript zu arbeiten: Bun bündelt beispielsweise den TypeScript-Compiler automatisch, so dass Sie nichts weiter installieren müssen. Und auch die Deno-Runtime bietet integrierten Support für TypeScript. Wenn Sie ohnehin schon mit dem Gedanken spielen, auf eines dieser Projekte umzusteigen, warum nicht gleich mit TypeScript? TypeScript in JavaScript kompilieren Den TypeScript-Compiler einfach auf bestehenden JavaScript-Code „loszulassen“ ist der einfachste Weg, diesen in TypeScript zu kompilieren: tsc myfile.ts TypeScript-Dateien verwenden die Dateiendung .ts. Wenn sie durch den Compiler laufen, werden sie in .js-Dateien umgewandelt – mit gleichem Namen am gleichen Platz. Für einzelne Dateien ist das in Ordnung. Wahrscheinlicher ist jedoch, dass Sie ein ganzes Projektverzeichnis mit diversen Dateien kompilieren wollen. Um das ohne großen Aufwand zu tun, müssen Sie eine simple Konfigurationsdatei für Ihr Projekt schreiben. Die TypeScript-Konfigurationsdatei heißt in aller Regel tsconfig.json und befindet sich im Stammverzeichnis Ihres Projekts. Eine einfache Variante könnte folgendermaßen aussehen: { "compilerOptions": { "outDir": "./jssrc", "allowJs": true, "target": "es6", "sourceMap": true }, "include": ["./src/**/*"] } In diesem Code-Snippet teilt compilerOptions dem Compiler mit, wo eine kompilierte Datei abgelegt werden soll. „outDir“: „./jssrc“ bedeutet, dass alle generierten .js-Dateien in einem Verzeichnis namens jssrc („JavaScript Source“) abgelegt werden. Sie können aber auch einen beliebigen Namen verwenden, der zu Ihrem Projektlayout passt. Außerdem wird mit „allowJs“: true festgelegt, dass reguläre JavaScript-Dateien als Input akzeptiert werden. Das gewährleistet, dass Sie JavaScript- und TypeScript-Dateien problemlos in Ihrem src-Ordner mischen können. Wenn Sie outDir nicht spezifizieren, werden die JavaScript-Dateien neben den entsprechenden TypeScript-Dateien im Quellverzeichnis abgelegt. Das ist unter Umständen ungünstig, etwa wenn Sie die generierten Dateien zu Debugging-Zwecken in einem separaten Verzeichnis ablegen möchten. In unserer Konfigurationsdatei können wir auch definieren, nach welchem ECMAScript-Standard kompiliert werden soll. „target“: „es6“ bedeutet, dass wir ECMAScript 6 (ES6) verwenden. Die meisten JavaScript-Engines und Browser unterstützen mittlerweile ES6 – das ist also eine akzeptable Standardeinstellung. „sourceMap“: true zu spezifizieren, generiert .js.map-Dateien zusammen mit allen generierten JavaScript-Dateien für die Fehlersuche. Last, but not least stellt der include-Abschnitt ein glob-Pattern zur Verfügung, um zu verarbeitende Quelldateien zu finden. Darüber hinaus bietet tsconfig.json zahlreiche weitere Optionen, aber die genannten sollten für den Einstieg völlig ausreichen. Nachdem Sie tsconfig.json eingerichtet haben, können Sie tsc im Stammverzeichnis des Projekts ausführen und Dateien in dem durch outDir spezifizierten Verzeichnis generieren. Dabei sollten Sie darauf achten, ein outDir zu spezifizieren, das nicht mit anderen Dateien in Konflikt steht. Typ-Annotationen hinzufügen Im nächsten Schritt gilt es, den existierenden JavaScript-Code schrittweise zu TypeScript zu migrieren. Da alle bestehenden JavaScript-Dateien bereits gültiges TypeScript sind, eine Datei nach der anderen bearbeiten, indem Sie bestehende .js– in .ts-Dateien umbenennen. Solange eine JavaScript-Datei keine Typ-Informationen oder andere TypeScript-spezifische Syntax enthält, wird der Compiler nicht tätig. Mit Blick auf TypScript-Annotationen ist ein möglicher Startpunkt, sie zu Funktionssignaturen und Rückgabetypen hinzuzufügen. Im Folgenden eine JavaScript-Funktion ohne Typ-Annotationen. Sie dient dazu, den Namen einer Person im Nachname-Vorname-Format zu generieren, basierend auf einem Objekt mit den Eigenschaften .firstName und .lastName. function lastNameFirst(person) { return `${person.lastName}, ${person.firstName}`; } Mit TypeScript lässt sich deutlich expliziter gestalten, was akzeptiert und zurückgegeben wird. Wir stellen lediglich Typ-Annotationen für die Argumente und den Rückgabewert zur Verfügung: function lastNameFirst(person: Person): string { return `${person.lastName}, ${person.firstName}`; } Dieser Code geht davon aus, dass wir zuvor im Code einen Objekttyp namens Person definiert haben. Außerdem nutzt er string sowohl in JavaScript als auch in TypeScript als integrierten Typ. Indem wir diese Annotationen hinzufügen, stellen wir sicher, dass jeder Code, der diese Funktion aufruft, ein Objekt vom Typ Person bereitstellen muss. Wird stattdessen DogBreed bereitstellt, gibt der Compiler folgende Fehlermeldung aus: error TS2345: Argument of type 'DogBreed' is not assignable to parameter of type 'Person'. Type 'DogBreed' is missing the following properties from type 'Person': firstName, lastName Mit den Fehlerdetails erhalten Sie nicht nur eine Warnung, dass es sich nicht um den richtigen Typ handelt: Sie bekommen auch Hinweise dazu, warum dieser Typ für eine bestimmte Instanz nicht funktioniert. Das liefert aber nicht einen Anhaltspunkt darüber, wie das unmittelbare Problem behoben werden kann. Es regt zudem dazu an, darüber nachzudenken, wie Typen erweitert oder eingeschränkt werden können, um sich an Use Cases anzupassen. Interface-Deklarationen Eine Interface Declaration bietet eine weitere Möglichkeit, zu beschreiben, welche Typen mit etwas verwendet werden können. Ein Interface (Schnittstelle) ermöglicht es, zu beschreiben, was erwartet werden kann, ohne das vollständig definieren zu müssen. Ein Beispiel: interface Name { firstName: string; lastName: string; } function lastNameFirst(person: Name): string { return `${person.lastName}, ${person.firstName}`; } In einem Fall wie diesem könnten wir jeden beliebigen Typ an lastnameFirst() übergeben, solange er die Eigenschaften .firstName und .lastName aufweist und es sich dabei um string-Typen handelt. So können Sie Typen erstellen, die sich auf die Form des verwendeten Objekts beziehen und nicht darauf, ob es sich um einen bestimmten Typ handelt. TypeScript-Typen identifizieren Wenn Sie JavaScript-Code mit Annotationen versehen, um TypeScript zu erstellen, werden Ihnen die meisten verwendeten Typinformationen bekannt sein, da sie aus JavaScript-Typen stammen. Wie und wo Sie diese Typen anwenden, muss jedoch gut überlegt sein. Im Allgemeinen müssen Sie keine Annotationen für Literale hinzufügen, weil die automatisch abgeleitet werden können. Beispielsweise ist name: string = „Davis“; redundant, da aus der Zuweisung zum Literal klar hervorgeht, dass name ein string ist. Bei vielen anonymen Funktionen können die Typen ebenfalls auf diese Weise abgeleitet werden. Primitive Typen – string, number und boolean – können auf Variablen angewendet werden, die diese Typen verwenden und bei denen sie nicht automatisch abgeleitet werden können. Für Arrays von Typen können Sie den Typ gefolgt von [] (beispielsweise number[] für ein Array von Zahlen) oder die Syntax Array verwenden (in diesem Fall Array). Eigene Typen definieren Sie mit dem Keyword type: type FullName = { firstName: string; lastName: string; }; Das ließe sich auch nutzen, um ein Objekt zu erstellen, das seiner Typform entspricht: var myname:FullName = {firstName:"Brad", lastName:"Davis"}; Allerdings würde das in einem Fehler resultieren: var myname:FullName = {firstName:"Brad", lastName:"Davis", middleName:"S."}; Der Grund: middleName ist in unserem Typ nicht definiert. Mit dem Operator | können Sie angeben, dass mehrere, verschiedene Typen möglich sind: type userName = Fullname | string; // or we can use it in a function signature ... function doSomethingWithName(name: Fullname|string) {...} Wenn Sie einen neuen Typ erstellen möchten, der eine aus bestehenden Typen zusammengesetzt ist (ein Intersection-Typ) nutzen Sie dazu den Operator &: type Person = { firstName: string; lastName: string; }; type Bibliography = { books: Array; }; type Author = Person & Bibliography; // we can then create an object that uses fields from both types: var a: Author = { firstName: "Serdar", lastName: "Yegulalp", books: ["Python Made Easy", "Python Made Complicated"] }; Zu beachten ist dabei, dass Sie zwar mit Typen so umgehen können – wenn Sie jedoch etwas Ähnliches mit Interfaces umsetzen wollen, müssen Sie einen anderen Ansatz verwenden. Der führt über das Keyword extends: interface Person { firstName: string; lastName: string; } interface Author extends Person { penName: string; } JavaScript-Klassen werden ebenfalls als Typen berücksichtigt. Mit TypeScript können Sie sie unverändert mit Typ-Annotationen verwenden: class Person { name: string; constructor( public firstName: string, public lastName: string ) { this.firstName = firstName; this.lastName = lastName; this.name = `${firstName} ${lastName}`; } } TypeScript verfügt außerdem auch über einige spezielle Typen für andere Fälle. any wird seinem Namen gerecht: Jeder Typ wird akzeptiert. null und undefined haben dieselbe Bedeutung wie in normalem JavaScript (beispielsweise würden Sie string|null verwenden, um einen Typ anzugeben, der entweder eine Zeichenfolge oder ein null-Wert ist. TypeScript unterstützt auch nativ den Postfix-Operator !. So stellt etwa x!.action() sicher, dass .action() auf x aufgerufen wird – solange x nicht null oder undefined ist. Wenn Sie auf eine Funktion verweisen möchten, die eine bestimmte Form von Type Expression nutzt, können Sie dazu eine sogenannte „Call-Signatur“ verwenden: function runFn(fn: (arg: number) => any, value: number): any { return fn(value); } runFn würde eine Funktion akzeptieren, die eine einzelne Zahl als Argument nimmt und einen beliebigen Wert zurückgibt. Beachten Sie, dass wir hier die Pfeilnotation verwenden, um anzugeben, was die übergebene Funktion zurückgibt, nicht einen Doppelpunkt, wie wir es in der Signatur der Hauptfunktion tun. TypeScript-Projekt aufbauen Viele Build-Tools im JavaScript-Ökosystem sind mittlerweile TypeScript-fähig: Die Frameworks tsdx, Angular und Nest können eine JavaScript-Codebasis mit wenig Aufwand automatisch in den passenden TypeScript-Code umwandeln. Wenn Sie mit einem Build-Tool wie Babel, webpack oder anderen arbeiten, können auch diese TypeScript-Projekte verarbeiten, insofern Sie TypeScript-Handling als Extension installieren oder manuell aktivieren. Um bestehende JavaScript-Projekte erfolgreich auf TypeScript umzustellen, empfiehlt es sich in erster Linie, Schritt für Schritt vorzugehen: Migrieren Sie jeweils ein Modul und dann eine Funktion. TypeScript und JavaScript können koexistieren – Sie müssen also nicht alles auf einmal migrieren. Nehmen Sie sich die Zeit, zu experimentieren und die besten Typen für Ihre Codebasis zu finden. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!