TypeScript, Minus TypeScript

Avatar of Caleb Williams
Caleb Williams am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wenn Sie nicht seit mehreren Jahren unter einem Felsen versteckt waren (und seien wir ehrlich, unter einem Felsen zu verstecken fühlt sich manchmal richtig an), haben Sie wahrscheinlich von TypeScript gehört und es wahrscheinlich auch benutzt. TypeScript ist eine syntaktische Obermenge von JavaScript, die – wie der Name schon sagt – dem beliebtesten Skripting-Sprache des Webs Typen hinzufügt.

TypeScript ist unglaublich leistungsfähig, aber für Anfänger oft schwer zu lesen und bringt den zusätzlichen Aufwand mit sich, dass vor der Ausführung im Browser ein Kompilierungsschritt erforderlich ist, aufgrund der zusätzlichen Syntax, die kein gültiges JavaScript ist. Für viele Projekte ist das kein Problem, aber für andere kann dies die Arbeit behindern. Glücklicherweise hat das TypeScript-Team eine Möglichkeit aktiviert, Vanilla JavaScript mit JSDoc zu typisieren.

Ein neues Projekt einrichten

Um TypeScript in einem neuen Projekt zum Laufen zu bringen, benötigen Sie NodeJS und npm. Beginnen wir damit, ein neues Projekt zu erstellen und `npm init` auszuführen. Für diesen Artikel werden wir VShttps://visualstudiocode.deCode als unseren Code-Editor verwenden. Sobald alles eingerichtet ist, müssen wir TypeScript installieren.

npm i -D typescript

Nachdem die Installation abgeschlossen ist, müssen wir TypeScript mitteilen, was es mit unserem Code tun soll. Erstellen wir also eine neue Datei namens tsconfig.json und fügen Sie Folgendes hinzu:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["es2017", "dom"],
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": false,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true
  },
  "include": [ "script", "test" ],
  "exclude": [ "node_modules" ]
}

Für unsere Zwecke sind die wichtigen Zeilen dieser Konfigurationsdatei die Optionen allowJs und checkJs, die beide auf true gesetzt sind. Diese teilen TypeScript mit, dass wir möchten, dass es unseren JavaScript-Code auswertet. Wir haben TypeScript auch angewiesen, alle Dateien in einem Verzeichnis /script zu prüfen. Lassen Sie uns dieses Verzeichnis und eine neue Datei darin namens index.js erstellen.

Ein einfaches Beispiel

In unserer neu erstellten JavaScript-Datei erstellen wir eine einfache Additionsfunktion, die zwei Parameter nimmt und sie addiert.

function add(x, y) {
  return x + y;
}

Ziemlich einfach, oder? add(4, 2) gibt 6 zurück, aber da JavaScript dynamisch typisiert ist, könnten Sie `add` auch mit einem String und einer Zahl aufrufen und potenziell unerwartete Ergebnisse erhalten.

add('4', 2); // returns '42'

Das ist weniger als ideal. Glücklicherweise können wir unserer Funktion JSDoc-Annotationen hinzufügen, um Benutzern mitzuteilen, wie wir erwarten, dass sie funktioniert.

/**
 * Add two numbers together
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
function add(x, y) {
  return x + y;
}

Wir haben nichts an unserem Code geändert; wir haben lediglich einen Kommentar hinzugefügt, um Benutzern mitzuteilen, wie die Funktion verwendet werden soll und welcher Wert erwartet wird. Dies haben wir durch die Verwendung der JSDoc-Annotationen @param und @return mit in geschweiften Klammern gesetzten Typen ({}) erreicht.

Der Versuch, unseren fehlerhaften Schnipsel von zuvor auszuführen, führt in VS Code zu einem Fehler.

TypeScript wertet einen Aufruf von add als falsch aus, wenn eines der Argumente ein String ist.

Im obigen Beispiel liest TypeScript unseren Kommentar und überprüft ihn für uns. In echtem TypeScript ist unsere Funktion nun gleichbedeutend mit dem Schreiben von

/**
 * Add two numbers together
 */
function add(x: number, y: number): number {
  return x + y;
}

Genauso wie wir den Typ number verwendet haben, haben wir Zugriff auf Dutzende von integrierten Typen mit JSDoc, darunter String, Object, Array sowie viele andere, wie z. B. HTMLElement, MutationRecord und mehr.

Ein zusätzlicher Vorteil der Verwendung von JSDoc-Annotationen gegenüber der proprietären Syntax von TypeScript ist, dass sie Entwicklern die Möglichkeit bietet, zusätzliche Metadaten zu Argumenten oder Typdefinitionen bereitzustellen, indem diese inline angegeben werden (in der Hoffnung, positive Gewohnheiten zur Selbstdokumentation unseres Codes zu fördern).

Wir können TypeScript auch mitteilen, dass Instanzen bestimmter Objekte Erwartungen haben könnten. Eine WeakMap beispielsweise ist ein integriertes JavaScript-Objekt, das eine Zuordnung zwischen einem beliebigen Objekt und einer beliebigen anderen Datenmenge erstellt. Diese zweite Datenmenge kann standardmäßig alles sein, aber wenn wir möchten, dass unsere WeakMap-Instanz nur einen String als Wert akzeptiert, können wir TypeScript mitteilen, was wir wollen.

/** @type {WeakMap<object>, string} */
const metadata = new WeakMap();


const object = {};
const otherObject = {};


metadata.set(object, 42);
metadata.set(otherObject, 'Hello world');

Dies führt zu einem Fehler, wenn wir versuchen, unsere Daten auf 42 zu setzen, da es kein String ist.

Eigene Typen definieren

Genau wie TypeScript erlaubt uns JSDoc, eigene Typen zu definieren und mit ihnen zu arbeiten. Lassen Sie uns einen neuen Typ namens Person erstellen, der die Eigenschaften name, age und hobby hat. So sieht das in TypeScript aus:

interface Person {
  name: string;
  age: number;
  hobby?: string;
}

In JSDoc wäre unser Typ wie folgt:

/**
 * @typedef Person
 * @property {string} name - The person's name
 * @property {number} age - The person's age
 * @property {string} [hobby] - An optional hobby
 */

Wir können das Tag @typedef verwenden, um den name unseres Typs zu definieren. Lassen Sie uns eine Schnittstelle namens Person mit den erforderlichen name (ein String) und age (eine Zahl) Eigenschaften definieren, sowie einer dritten optionalen Eigenschaft namens hobby (ein String). Um diese Eigenschaften zu definieren, verwenden wir @property (oder die Kurzform @prop Schlüssel) innerhalb unseres Kommentars.

Wenn wir den Typ Person mit dem Kommentar @type auf ein neues Objekt anwenden, erhalten wir beim Schreiben unseres Codes Typüberprüfung und Autovervollständigung. Nicht nur das, wir werden auch darüber informiert, wenn unser Objekt nicht mit dem Vertrag übereinstimmt, den wir in unserer Datei definiert haben.

Screenshot of an example of TypeScript throwing an error on our vanilla JavaScript object

Nun wird das Vervollständigen des Objekts den Fehler beheben.

Unser Objekt entspricht nun der oben definierten Person-Schnittstelle.

Manchmal möchten wir jedoch kein vollständiges Objekt für einen Typ. Zum Beispiel möchten wir vielleicht eine begrenzte Auswahl an Optionen bereitstellen. In diesem Fall können wir etwas nutzen, das als Union-Typ bezeichnet wird.

/**
 * @typedef {'cat'|'dog'|'fish'} Pet
 */


/**
 * @typedef Person
 * @property {string} name - The person's name
 * @property {number} age - The person's age
 * @property {string} [hobby] - An optional hobby
 * @property {Pet} [pet] - The person's pet
 */

In diesem Beispiel haben wir einen Union-Typ namens Pet definiert, der eine der möglichen Optionen 'cat', 'dog' oder 'fish' sein kann. Andere Tiere in unserer Umgebung sind als Haustiere nicht erlaubt. Wenn caleb oben versuchen würde, einen 'kangaroo' in seinem Haushalt aufzunehmen, würden wir einen Fehler erhalten.

/** @type {Person} */
const caleb = {
  name: 'Caleb Williams',
  age: 33,
  hobby: 'Running',
  pet: 'kangaroo'
};
Screenshot of an an example illustrating that kangaroo is not an allowed pet type

Diese gleiche Technik kann verwendet werden, um verschiedene Typen in einer Funktion zu mischen.

/**
 * @typedef {'lizard'|'bird'|'spider'} ExoticPet
 */


/**
 * @typedef Person
 * @property {string} name - The person's name
 * @property {number} age - The person's age
 * @property {string} [hobby] - An optional hobby
 * @property {Pet|ExoticPet} [pet] - The person's pet
 */

Nun kann unser Person-Typ entweder ein Pet oder ein ExoticPet sein.

Arbeiten mit Generics

Es kann vorkommen, dass wir keine festen Typen, sondern etwas mehr Flexibilität wünschen, während wir trotzdem konsistenten, stark typisierten Code schreiben. Hier kommen generische Typen ins Spiel. Das klassische Beispiel für eine generische Funktion ist die Identitätsfunktion, die ein Argument nimmt und es dem Benutzer zurückgibt. In TypeScript sieht das so aus:

function identity<T>(target: T): T {
  return target;
}

Hier definieren wir einen neuen generischen Typ (T) und teilen dem Computer und unseren Benutzern mit, dass die Funktion einen Wert zurückgibt, der denselben Typ wie das Argument target hat. Auf diese Weise können wir immer noch eine Zahl, einen String oder ein HTMLElement übergeben und sicher sein, dass der zurückgegebene Wert ebenfalls vom gleichen Typ ist.

Dasselbe ist mit der JSDoc-Notation unter Verwendung der Annotation @template möglich.

/**
 * @template T
 * @param {T} target
 * @return {T}
 */
function identity(target) {
  return x;
}

Generics sind ein komplexes Thema. Für detailliertere Dokumentationen zur Verwendung von Generics in JSDoc, einschließlich Beispielen, können Sie die Google Closure Compiler-Seite zu diesem Thema lesen.

Typumwandlung (Type Casting)

Obwohl starke Typisierung oft sehr hilfreich ist, stellen Sie möglicherweise fest, dass die integrierten Erwartungen von TypeScript nicht ganz zu Ihrem Anwendungsfall passen. In solchen Fällen müssen wir möglicherweise ein Objekt in einen neuen Typ umwandeln. Ein Beispiel, wann dies notwendig sein könnte, ist bei der Arbeit mit Ereignis-Listenern.

In TypeScript nehmen alle Ereignis-Listener eine Funktion als Callback entgegen, wobei das erste Argument ein Objekt vom Typ Event ist, das eine Eigenschaft `target` hat, die ein EventTarget ist. Dies ist der korrekte Typ gemäß dem DOM-Standard, aber oft existiert die Information, die wir aus dem Ziel des Ereignisses extrahieren möchten, nicht auf EventTarget – wie die Eigenschaft `value`, die auf HTMLInputElement.prototype existiert. Das macht den folgenden Code ungültig:

document.querySelector('input').addEventListener(event => {
  console.log(event.target.value);
};

TypeScript wird sich darüber beschweren, dass die Eigenschaft value nicht auf EventTarget existiert, obwohl wir als Entwickler sehr gut wissen, dass ein <input> einen value hat.

A screenshot showing that value doesn’t exist on type EventTarget

Damit wir TypeScript mitteilen können, dass wir wissen, dass event.target ein HTMLInputElement sein wird, müssen wir den Typ des Objekts umwandeln.

document.getElementById('input').addEventListener('input', event => {
  console.log(/** @type {HTMLInputElement} */(event.target).value);
});

Wenn wir event.target in Klammern einschließen, trennen wir es vom Aufruf von value. Das Hinzufügen des Typs vor den Klammern teilt TypeScript mit, dass wir meinen, dass event.target etwas anderes ist als das, was es normalerweise erwartet.

Screenshot of a valid example of type casting in VS Code.

Und wenn ein bestimmtes Objekt problematisch ist, können wir TypeScript jederzeit mitteilen, dass ein Objekt @type {any} ist, um Fehlermeldungen zu ignorieren, obwohl dies im Allgemeinen als schlechte Praxis gilt, auch wenn es im Notfall nützlich ist.

Zusammenfassung

TypeScript ist ein unglaublich leistungsfähiges Werkzeug, das viele Entwickler nutzen, um ihren Workflow rund um konsistente Code-Standards zu optimieren. Während die meisten Anwendungen den integrierten Compiler nutzen werden, entscheiden sich einige Projekte möglicherweise dafür, dass die zusätzliche Syntax, die TypeScript bietet, im Weg ist. Oder vielleicht fühlen sie sich einfach wohler, sich an Standards zu halten, anstatt an eine erweiterte Syntax gebunden zu sein. In diesen Fällen können Entwickler immer noch die Vorteile der Verwendung des Typsystems von TypeScript nutzen, auch wenn sie Vanilla JavaScript schreiben.