TypeScript vs. JavaScript: Ihr Leitfaden
Veröffentlicht: 2022-03-11TypeScript oder JavaScript? Entwickler erwägen diese Wahl für Greenfield-Web- oder Node.js-Projekte, aber es ist auch eine Frage, die es wert ist, für bestehende Projekte in Betracht gezogen zu werden. TypeScript ist eine Obermenge von JavaScript und bietet alle Funktionen von JavaScript sowie einige zusätzliche Vorteile. TypeScript ermutigt uns von Natur aus, sauber zu programmieren, wodurch der Code skalierbarer wird. Projekte können jedoch so viel reines JavaScript enthalten, wie wir möchten, sodass die Verwendung von TypeScript keine Alles-oder-Nichts-Angelegenheit ist.
Die Beziehung zwischen TypeScript und JavaScript
TypeScript fügt JavaScript ein explizites Typsystem hinzu, das die strenge Durchsetzung von Variablentypen ermöglicht. TypeScript führt seine Typprüfungen beim Transpilieren durch – eine Form der Kompilierung, die TypeScript-Code in den JavaScript-Code konvertiert, den Webbrowser und Node.js verstehen.
Beispiele für TypeScript und JavaScript
Beginnen wir mit einem gültigen JavaScript-Snippet:
let var1 = "Hello"; var1 = 10; console.log(var1); Hier beginnt var1 als string und wird dann zu einer number .
Da JavaScript nur lose typisiert ist, können wir var1 als Variable eines beliebigen Typs umdefinieren – von einem String bis zu einer Funktion.
Die Ausführung dieses Codes gibt 10 aus.
Lassen Sie uns nun diesen Code in TypeScript ändern:
let var1: string = "Hello"; var1 = 10; console.log(var1); In diesem Fall deklarieren wir var1 als string . Wir versuchen dann, ihm eine Zahl zuzuweisen, was das strenge Typsystem von TypeScript nicht erlaubt. Das Transpilieren führt zu einem Fehler:
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; Wenn wir den Transpiler anweisen würden, das ursprüngliche JavaScript-Snippet so zu behandeln, als wäre es TypeScript, würde der Transpiler automatisch schlussfolgern, dass var1 ein string | number sein sollte string | number . Dies ist ein TypeScript -Vereinigungstyp , der es uns ermöglicht, var1 einen string oder eine number zuzuweisen. Nachdem der Typkonflikt gelöst wurde, würde unser TypeScript-Code erfolgreich transpilieren. Die Ausführung würde das gleiche Ergebnis wie das JavaScript-Beispiel erzeugen.
TypeScript vs. JavaScript aus 30.000 Fuß: Skalierbarkeitsherausforderungen
JavaScript ist allgegenwärtig und treibt Projekte aller Größenordnungen an, die auf eine Weise angewendet werden, die in den Kinderschuhen der 1990er Jahre undenkbar gewesen wäre. Obwohl JavaScript ausgereift ist, ist es in Bezug auf die Unterstützung der Skalierbarkeit unzureichend. Dementsprechend müssen sich Entwickler mit JavaScript-Anwendungen auseinandersetzen, die sowohl an Umfang als auch an Komplexität zugenommen haben.
Glücklicherweise adressiert TypeScript viele der Probleme bei der Skalierung von JavaScript-Projekten. Wir konzentrieren uns auf die drei größten Herausforderungen: Validierung, Refactoring und Dokumentation.
Validierung
Wir verlassen uns auf integrierte Entwicklungsumgebungen (IDEs), um bei Aufgaben wie dem Hinzufügen, Ändern und Testen von neuem Code zu helfen, aber IDEs können keine reinen JavaScript-Referenzen validieren. Wir mindern dieses Manko, indem wir beim Codieren wachsam überwachen, um die Möglichkeit von Tippfehlern in Variablen und Funktionsnamen auszuschließen.
Das Ausmaß des Problems wächst exponentiell, wenn der Code von einem Drittanbieter stammt, wo fehlerhafte Referenzen in selten ausgeführten Codezweigen leicht unentdeckt bleiben könnten.
Im Gegensatz dazu können wir uns mit TypeScript auf die Codierung konzentrieren und darauf vertrauen, dass alle Fehler zum Zeitpunkt der Transpilation erkannt werden. Um dies zu demonstrieren, beginnen wir mit etwas altem JavaScript-Code:
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } Der .toISO() ist ein Tippfehler der moment.js toISOString() -Methode, aber der Code würde funktionieren, vorausgesetzt, das format -Argument ist nicht ISO . Wenn wir zum ersten Mal versuchen, ISO an die Funktion zu übergeben, wird dieser Laufzeitfehler ausgegeben: TypeError: moment(...).toISO is not a function .
Das Auffinden eines falsch geschriebenen Codes kann schwierig sein. Die aktuelle Codebasis hat möglicherweise keinen Pfad zu der unterbrochenen Zeile, in diesem Fall würde unsere defekte .toISO() Referenz beim Testen nicht erfasst werden.
Wenn wir diesen Code nach TypeScript portieren, würde die IDE die defekte Referenz hervorheben und uns auffordern, Korrekturen vorzunehmen. Wenn wir nichts tun und versuchen zu transpilieren, würden wir blockiert und der Transpiler würde den folgenden Fehler generieren:
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());Refactoring
Während Tippfehler in Codereferenzen von Drittanbietern nicht ungewöhnlich sind, gibt es eine andere Reihe von Problemen im Zusammenhang mit Tippfehlern in internen Referenzen, wie zum Beispiel dieses:
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } Ein einzelner Entwickler kann alle Instanzen von phoneNumbr so finden und reparieren, dass sie mit er enden.
Aber je größer das Team, desto teurer wird dieser einfache, häufige Fehler. Im Rahmen ihrer Arbeit müssten die Kollegen auf solche Tippfehler aufmerksam werden und sie verbreiten. Alternativ würde das Hinzufügen von Code zur Unterstützung beider Schreibweisen die Codebasis unnötig aufblähen.
Wenn wir mit TypeScript einen Tippfehler beheben, wird abhängiger Code nicht mehr transpiliert, was Kollegen signalisiert, die Korrektur an ihren Code weiterzugeben.
Dokumentation
Genaue und relevante Dokumentation ist der Schlüssel zur Kommunikation innerhalb und zwischen Entwicklerteams. JavaScript-Entwickler verwenden häufig JSDoc, um erwartete Methoden- und Eigenschaftstypen zu dokumentieren.
Die Sprachfeatures von TypeScript (z. B. abstrakte Klassen, Schnittstellen und Typdefinitionen) erleichtern die Design-by-Contract-Programmierung und führen zu einer qualitativ hochwertigen Dokumentation. Darüber hinaus hilft eine formale Definition der Methoden und Eigenschaften, denen ein Objekt entsprechen muss, Breaking Changes zu identifizieren, Tests zu erstellen, Code-Introspektion durchzuführen und Architekturmuster zu implementieren.
Für TypeScript extrahiert das Einstiegstool TypeDoc (basierend auf dem TSDoc-Vorschlag) automatisch Typinformationen (z. B. Klasse, Schnittstelle, Methode und Eigenschaft) aus unserem Code. So erstellen wir mühelos eine Dokumentation, die weitaus umfassender ist als die von JSDoc.
Vorteile von TypeScript gegenüber JavaScript
Lassen Sie uns nun untersuchen, wie wir TypeScript verwenden können, um diese Skalierbarkeitsherausforderungen zu bewältigen.
Erweiterte Code-/Refactoring-Vorschläge
Viele IDEs können Informationen aus dem TypeScript-Typsystem verarbeiten und so beim Codieren eine Referenzvalidierung bereitstellen. Noch besser: Während wir tippen, kann die IDE relevante Dokumentationen auf einen Blick (z. B. die Argumente, die eine Funktion erwartet) für jede Referenz liefern und kontextuell korrekte Variablennamen vorschlagen.
In diesem TypeScript-Snippet schlägt die IDE eine automatische Vervollständigung der Namen der Schlüssel im Rückgabewert der Funktion vor:
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }Meine IDE, Visual Studio Code, lieferte diesen Vorschlag (im Callout), als ich begann, die Funktion aufzurufen (Zeile 31):
Darüber hinaus sind die Autocomplete-Vorschläge der IDE (im Callout) kontextuell korrekt und zeigen nur gültige Namen innerhalb einer verschachtelten Schlüsselsituation (Zeile 34):
Solche Echtzeitvorschläge führen zu einer schnelleren Codierung. Darüber hinaus können sich IDEs auf die strengen Typinformationen von TypeScript verlassen, um Code in jeder Größenordnung umzugestalten. Vorgänge wie das Umbenennen einer Eigenschaft, das Ändern von Dateispeicherorten oder sogar das Extrahieren einer Oberklasse werden trivial, wenn wir uns zu 100 % auf die Genauigkeit unserer Referenzen verlassen können.

Schnittstellenunterstützung
Im Gegensatz zu JavaScript bietet TypeScript die Möglichkeit, Typen über Schnittstellen zu definieren. Eine Schnittstelle listet die Methoden und Eigenschaften, die ein Objekt enthalten muss, formal auf, implementiert sie aber nicht. Dieses Sprachkonstrukt ist besonders hilfreich für die Zusammenarbeit mit anderen Entwicklern.
Das folgende Beispiel zeigt, wie wir die Funktionen von TypeScript nutzen können, um gängige OOP-Muster sauber zu implementieren – in diesem Fall Strategie und Verantwortungskette – und so das vorherige Beispiel zu verbessern:
export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }ES6-Module – überall
Zum jetzigen Zeitpunkt unterstützen nicht alle Front-End- und Back-End-JavaScript-Laufzeiten ES6-Module. Mit TypeScript können wir jedoch die ES6-Modulsyntax verwenden:
import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); Die transpilierte Ausgabe ist mit unserer ausgewählten Umgebung kompatibel. Wenn wir beispielsweise die Compiler-Option --module CommonJS , erhalten wir:
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; Wenn Sie stattdessen --module UMD verwenden, gibt TypeScript das ausführlichere UMD-Muster aus:
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });ES6-Klassen – überall
Ältere Umgebungen haben oft keine Unterstützung für ES6-Klassen. Eine TypeScript-Transpile stellt die Kompatibilität sicher, indem zielspezifische Konstrukte verwendet werden. Hier ist ein TypeScript-Quellcode-Snippet:
export class TestClass { hello = 'World'; }Die JavaScript-Ausgabe hängt sowohl vom Modul als auch vom Ziel ab, das wir mit TypeScript angeben können.
Folgendes liefert --module CommonJS --target es3 :
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; Wenn Sie stattdessen --module CommonJS --target es6 verwenden, erhalten wir das folgende transpilierte Ergebnis. Das Schlüsselwort class wird verwendet, um auf ES6 abzuzielen:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;Async/Await-Funktionalität – überall
Async/await erleichtert das Verständnis und die Wartung von asynchronem JavaScript-Code. TypeScript bietet diese Funktionalität für alle Laufzeiten, auch für diejenigen, die async/await nicht nativ bereitstellen.
Beachten Sie, dass Sie zum Ausführen von async/await auf älteren Laufzeiten wie ES3 und ES5 externe Unterstützung für Promise -basierte Ausgabe benötigen (z. B. über Bluebird oder ein ES2015-Polyfill). Das mit TypeScript gelieferte Promise -Polyfill lässt sich problemlos in die transpilierte Ausgabe integrieren – wir müssen nur die lib -Compileroption entsprechend konfigurieren.
Unterstützung für private Klassenfelder – überall
Selbst für Legacy-Ziele unterstützt TypeScript private Felder auf ähnliche Weise wie stark typisierte Sprachen (z. B. Java oder C#). Im Gegensatz dazu unterstützen viele JavaScript-Laufzeiten private Felder durch die Hash-Präfix-Syntax, die ein fertiger Vorschlag von ES2022 ist.
Nachteile von TypeScript gegenüber JavaScript
Nachdem wir nun die wichtigsten Vorteile der Implementierung von TypeScript hervorgehoben haben, wollen wir Szenarien untersuchen, in denen TypeScript möglicherweise nicht die richtige Lösung ist.
Transpilation: Potenzielle Workflow-Inkompatibilität
Bestimmte Workflows oder Projektanforderungen sind möglicherweise nicht mit dem Transpilationsschritt von TypeScript kompatibel: zum Beispiel, wenn wir ein externes Tool verwenden mussten, um den Code nach der Bereitstellung zu ändern, oder wenn die generierte Ausgabe entwicklerfreundlich sein muss.
Beispielsweise habe ich kürzlich eine AWS Lambda-Funktion für eine Node.js-Umgebung geschrieben. TypeScript passte schlecht, weil eine erforderliche Transpilation mich und andere Teammitglieder daran hindern würde, die Funktion mit dem AWS-Online-Editor zu bearbeiten. Dies war ein Deal Breaker für den Projektmanager.
Geben Sie das System nur bis zur Transpile-Zeit ein
Die JavaScript-Ausgabe von TypeScript enthält keine Typinformationen, sodass keine Typprüfungen durchgeführt werden und daher die Typsicherheit zur Laufzeit unterbrochen werden kann. Angenommen, eine Funktion ist so definiert, dass sie immer ein Objekt zurückgibt. Wenn null von seiner Verwendung in einer .js -Datei zurückgegeben wird, tritt ein Laufzeitfehler auf.
Typinformationsabhängige Merkmale (z. B. private Felder, Schnittstellen oder Generika) fügen Wert zu jedem Projekt hinzu, werden aber beim Transpilieren abgekratzt. Zum Beispiel wären private Klassenmitglieder nach der Transpilation nicht länger privat. Um es klar zu sagen, Laufzeitprobleme dieser Art sind nicht nur bei TypeScript zu finden, und Sie können davon ausgehen, dass Sie auch bei JavaScript auf die gleichen Schwierigkeiten stoßen.
Kombination von TypeScript und JavaScript
Trotz der vielen Vorteile von TypeScript können wir es manchmal nicht rechtfertigen, ein ganzes JavaScript-Projekt auf einmal zu konvertieren. Glücklicherweise können wir dem TypeScript-Transpiler Datei für Datei angeben, was als reines JavaScript interpretiert werden soll. Tatsächlich kann dieser hybride Ansatz dazu beitragen, individuelle Herausforderungen zu mindern, die im Laufe des Lebenszyklus eines Projekts auftreten.
Wir ziehen es möglicherweise vor, JavaScript unverändert zu lassen, wenn der Code:
- Wurde von einem ehemaligen Kollegen geschrieben und würde einen erheblichen Reverse-Engineering-Aufwand erfordern, um in TypeScript konvertiert zu werden.
- Verwendet Techniken, die in TypeScript nicht erlaubt sind (z. B. fügt eine Eigenschaft nach der Objektinstanziierung hinzu) und würde eine Umgestaltung erfordern, um die TypeScript-Regeln einzuhalten.
- Gehört einem anderen Team an, das weiterhin JavaScript verwendet.
In solchen Fällen gibt eine Deklarationsdatei ( .d.ts -Datei, manchmal auch als Definitionsdatei oder Typisierungsdatei bezeichnet) TypeScript genügend Typdaten, um IDE-Vorschläge zu aktivieren, während der JavaScript-Code unverändert bleibt.
Viele JavaScript-Bibliotheken (z. B. Lodash, Jest und React) stellen TypeScript-Typisierungsdateien in separaten Typpaketen bereit, während andere (z. B. Moment.js, Axios und Luxon) Typisierungsdateien in das Hauptpaket integrieren.
TypeScript vs. JavaScript: Eine Frage der Rationalisierung und Skalierbarkeit
Die unübertroffene Unterstützung, Flexibilität und Erweiterungen, die durch TypeScript verfügbar sind, verbessern die Entwicklererfahrung erheblich und ermöglichen die Skalierung von Projekten und Teams. Die Hauptkosten für die Integration von TypeScript in ein Projekt sind die Hinzufügung des Transpilations-Erstellungsschritts. Für die meisten Anwendungen ist das Transpilieren in JavaScript kein Problem; Vielmehr ist es ein Sprungbrett zu den vielen Vorteilen von TypeScript.
Weiterführende Literatur im Toptal Engineering Blog:
- Arbeiten mit TypeScript und Jest Support: Ein AWS SAM-Tutorial
