TypeScript 3.9 bringt verbessertes Promise-Handling und neue Kommentar-Features
TypeScript basiert auf Vorschlägen zum ES6-Standard. Viele Konzepte der Sprache wie Klassen, Vererbung oder Module sind daher deckungsgleich mit ES6. Syntax und Semantik der Sprache sind stark an JavaScript angelehnt. Die Lernkurve ist für Entwickler, die von JavaScript zu TypeScript wechseln, relativ moderat.
Wer mit TypeScript arbeitet, kann dabei bereits existenten JavaScript-Code und die meisten Frameworks und Libraries aus dem JavaScript-Ökosystem nutzen. TypeScript-Code kann über JavaScript aufgerufen werden. Am Ende wird TypeScript zu JavaScript-Code kompiliert, der in allen Browsern, in Node.js und in jeder JavaScript-Engine läuft, die ECMAScript ab Version 3 unterstützt. Version 3.9 ist jetzt als Beta-Release verfügbar. Die meisten Neuerungen zielen auf Performanz und Stabilität sowie eine verbesserte Dev-Experience ab, viele Bug-Fixes und Vorschläge kommen dieses Mal aus der Community.
Verbesserter Inferenzprozess und Promise.all
In TypeScript 3.7 und später wurden Funktionsaufrufe wie Promise.all
und Promise.race
aktualisiert. Damit gingen allerdings einige Überraschungen einher, vor allem, wenn Values mit null
oder undefined
im Spiel waren.
interface Lion {
roar(): void
}
interface Seal {
singKissFromARose(): void
}
async function visitZoo(lionExhibit: Promise, sealExhibit: Promise) {
let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);
lion.roar(); // uh oh
// ~~~~
// Object is possibly 'undefined'.
}
*alle Code-Beispiele wurden aus dem offiziellen Blogpost zum Release übernommen.
Dass SealExhibit
ein undefined
enthält, geht im Beispiel komischerweise auf Lion
über und ihr bekommt die Fehlermeldung Object is possibly undefined
. Dieses unerwünschte Verhalten wurde in V 3.9 dank eines Pull-Requests von Jack Bates elimiert, sodass der oben stehende Code jetzt keine Fehler mehr wirft. Wer aufgrund derartiger Probleme mit Promises bei Versionen älter 3.7 geblieben ist, kann mit V 3.9 jetzt auch endlich upgraden.
Verbesserte Performanz
TypeScript 3.9 bringt einige Performanz-Upgrades. In vorherigen Versionen gab es diesbezüglich wohl Probleme im Zusammenhang mit Packages wie zum Beispiel material-ui oder styled-components. Über Pull-Requests konnten laut dem offiziellen Blogpost zum Release Probleme mit großen unions, Intersections, conditionals und mapped-Typen behoben werden und so pro Pull-Request eine Reduzierung der Kompilierzeit von fünf bis zehn Prozent erreicht werden. Material-UIs Compile-Time zum Beispiel ist damit in V 3.9 um etwa 40 Prozent reduziert.
Auch was den Editor angeht, kommt TS mit einigen Performanz-Verbesserungen. Beim Umbenennen einer Datei konnte es wohl bei der Arbeit mit bisherigen TS-Versionen passieren, dass VS Code bis zu zehn Sekunden dafür brauchte, herauszufinden, welche import
-Statements aktualisiert werden müssen. In V 3.9 wurde geändert, wie Datei-Lookups von Compiler und Language-Services gecached werden, was zu einer Verbesserung der Dev-Experience für TypeScript-Entwickler und -Entwicklerinnen führen sollte.
// @ts-expect-error-Kommentare
TypeScript 3.9 bringt ein neues Feature namens @ts-expect-error
-Kommentare. Nützlich ist das zum Beispiel, wenn ihr eine Library in TypeScript schreibt. Als Teil eures öffentlichen API exportiert ihr eine Funktion namens doStuff. Um Type-Checking-Errors für TypeScript-Entwicklerinnen zu ermöglichen, akzeptiert die Funktion zwei Strings als Parameter. Außerdem wird ein Laufzeitcheck durchgeführt, um auch JS-Entwicklern eine hilfreiche Fehlermeldung bieten zu können.
function doStuff(abc: string, xyz: string) {
assert(typeof abc === "string");
assert(typeof xyz === "string");
// do some stuff
}
Bei falscher Verwendung dieser Funktion bekommen TypeScript-User also ein hilfreiches kleines rotes Rechteck und JS-Nutzer einen Assertion Error. Um dieses Verhalten zu testen, schreibt ihr einen Unit-Test:
expect(() => {
doStuff(123, 456);
}).toThrow();
Schreibt ihr den Unit-Test jedoch in TypeScript, bekommt ihr folgende Fehlermeldung:
doStuff(123, 456);
// ~~~
// error: Type 'number' is not assignable to type 'string'.
Der @ts-expect-error
-Kommentar wurde in TypeScript 3.9 eingeführt, um genau das zu vermeiden. Den Kommentar könnt ihr einer Zeile voranstellen. Das bewirkt, dass ein TS-Error unterdrückt, das heißt, nicht angezeigt wird, falls es andernfalls einen gegeben hätte. Wenn nicht, führt das neue Feature dazu, dass TypeScript euch sagt, dass sein Einsatz nicht nötig gewesen wäre.
Um das zu veranschaulichen: Dieser Code wird ohne Fehlermeldung ausgeführt,
// @ts-expect-error
console.log(47 * "octopus");
während folgender Code:
// @ts-expect-error
console.log(1 + 1);
zu dieser Fehlermeldung führt:
Unused '@ts-expect-error' directive.
Alle Details dazu könnt ihr im zugehörigen Pull-Request nachlesen.
Wer sich jetzt fragt, wie sich @ts-ignore und @ts-expect-error unterscheiden: Der Unterschied ist, dass @ts-ignore Kommentare nichts tun, wenn die betreffende Zeile korrekt ist. Welchen ihr nutzt, ist abhängig von einigen Faktoren, eine Entscheidungshilfe findet ihr im offiziellen Blogpost zum Release.
Jetzt auch für Ternary Conditionals: Uncalled Function Checks
In TypeScript 3.7 wurde mit uncalled Function Checks ein Feature eingeführt, das dazu führt, dass ihr eine Fehlermeldung bekommt, wenn ihr den Funktionsaufruf vergessen habt.
function hasImportantPermissions(): boolean {
// ...
}
// Oops!
if (hasImportantPermissions) {
// ~~~~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead?
deleteAllTheImportantFiles();
}
Bislang war die Fehlermeldung allerdings Bedingungen in if
-Statements vorbehalten, jetzt wird sie auch in Ternary Conditionals unterstützt – gemeint ist folgende Schreibweise:
cond ? trueExpr : falseExpr;
Breaking Changes
Wie jedes Major Release kommt auch die neue TypeScript-Version mit einigen Breaking Changes.
Parsing-Unterschiede in Optional Chaining und Non-Null Assertions
Eine davon den erst kürzlich eingeführten Optional-Chaining Operator ?
. Kombiniert mit dem nicht-Null Assertion-Operatior !
führte er zu einem Verhalten, das Entwickler als kontraintuitiv empfanden.
Folgendes Beispiel
foo?.bar!.baz
wurde interpretiert wie der folgende JS-Code:
(foo?.bar).baz
Das Problem: Die Klammern stoppen das erwünschte Kurzschluss-Verhalten des Optional Chaining, so dass im Fall von foo = undefined
, der Zugriff auf baz
zu einem Laufzeitfehler führte. Der Hinweis auf dieses Verhalten kam aus dem Babel-Team. Die meisten waren der Meinung, dass der !
-Operator einfach weggelassen werden sollte, das Codesnippet also als foo?.bar.baz
interpretiert werden soll. Ist foo
hier undefined
, evaluiert folglich die ganze Funktion einfach zu undefined
.
Das ist zwar ein Breaking Change, allerdings einer, bei dem davon auszugehen ist, dass viele Entwickler ihren Code mit der neuen Interpretation im Hinterkopf geschrieben haben. Wer das frühere Verhalten weiter nutzen will, erreicht das über Klammern um alles, was sich links des !
-Operators befindet:
(foo?.bar)!.baz
} und > sind jetzt keine validen JSX-Text-Zeichen mehr
In JSX ist die Verwendung der Zeichen }
und >
an Textpositionen nicht erlaubt. TypeScript und Babel gleichen sich dieser Regel an, die neue Art der Notation sieht vor, eine HTML-Escape-Sequenz oder sogenannte Literal Strings zu nutzen. Zudem bekommt ihr dann auch gleich eine nützliche Fehlermeldung, zum Beispiel so:
let directions =
Navigate to: Menu Bar > Tools > Options
// ~ ~
// Unexpected token. Did you mean `{'>'}` or `>`?
Praktischerweise kommt der vorgeschlagene Quickfix mit einer „Fix all in File“-Option, für den Fall, dass der Fehler an mehreren Stellen innerhalb einer Datei auftaucht.
Striktere Checks für Intersections und Optional Properties
Es ist grundsätzlich möglich, Intersection-Types wie A & B
C
zuzuweisen, manchmal kommt es dabei allerdings zu Problemen mit sogenannten Optional Properties. Der folgende Code:
interface A {
a: number; // notice this is 'number'
}
interface B {
b: string;
}
interface C {
a?: boolean; // notice this is 'boolean'
b: string;
}
declare let x: A & B;
declare let y: C;
y = x;
war in früheren TypeScript-Versionen erlaubt. Zwar ist A nicht mit C kompatibel, B aber schon, das reichte bislang aus. Das ändert sich mit TypeScript 3.9: Ist jeder Typ in einer Intersection ein konkreter Objekttyp, betrachtet das Typsystem alle Properties auf einmal. Im Fall des oben stehenden Codes würde es also bemerken, dass die Property a von A & B nicht mit der von C kompatibel ist und folgenden Fehler werfen:
Type 'A & B' is not assignable to type 'C'.
Types of property 'a' are incompatible.
Type 'number' is not assignable to type 'boolean | undefined'.
Intersections Reduced By Discriminant Properties
Es gibt Fälle, bei denen Types Values beschreiben, die gar nicht existieren:
declare function smushObjects<T, U>(x: T, y: U): T & U;
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
declare let x: Circle;
declare let y: Square;
let z = smushObjects(x, y);
console.log(z.kind);
Komisch an diesem Code-Snippet ist, dass es nunmal keinen Weg gibt, eine Intersection von Circle
und Square
herzustellen, weil deren kind
-fields einfach nicht kompatibel sind. Bisher wäre dieser Code aber so durchgegangen – der Type von kind
war dabei never
, einfach weil “circle” & “square”
Values beschreibt, die es nie geben könnte. In TypeScript 3.9 gibt es dafür striktere Regeln. Eine Intersection von Circle
und Square
wird vom Typsystem aufgrund der inkompatiblen kind
-Properties als nicht möglich erkannt. Anstatt dass jetzt der Type von z.kind
zu never
wird, wird jetzt der type von z
(also Circle & Square
) selbst zu never
und ihr bekommt folgende Fehlermeldung:
Property 'kind' does not exist on type 'never'.
Fälle, in denen die Neuerung sich als nicht abwärtskompatibel herausstellte, betrafen laut dem offiziellen Blogpost zum Release Code mit – wie im Beispiel – leicht fehlerhaften Typenangaben.
Getter und Setter sind nicht mehr numerierbar
Bisher wurden get und set Accessoren in Klassen so ausgegeben, dass sie numerierbar waren. Die ECMAScript-Spezifikation sieht aber vor, dass sie das nicht sind. Je nach Target konnte sich das Verhalten eures TypeScript-Codes demnach unterscheiden. Dank eines externen Pull-Requests ist TypeScript mit Version 3.9 näher an der ES-Spezifikation.
Verändertes Verhalten für Typenparameter, die any extenden
Bisher konnte ein auf any
beschränkter Typenparameter äquivalent zu any
gehandhabt werden.
function foo(arg: T) {
arg.spfjgerijghoied; // no error!
}
Dabei handelte es sich aber offenbar um ein Versehen, das mit V 3.9 aus der Welt geschafft wird:
function foo(arg: T) {
arg.spfjgerijghoied;
// ~~~~~~~~~~~~~~~
// Property 'spfjgerijghoied' does not exist on type 'T'.
}
export * wird ab V 3.9 immer mit ausgegeben
Bisher wurden Deklarationen wie export * from “foo”
im ausgegebenen JS weggelassen, wenn foo
keine Values exportiert. Das Problem dabei: Diese typengesteuerte Art der Ausgabe kann von Babel nicht emuliert werden. Mit der neuen Version werden export *
-Deklarationen deshalb einfach immer ausgegeben.
Neue Editor-Features
Außerdem kommt TypeScript 3.9 mit einigen neuen Editor-Features, darunter auto-Importe von CommonJS-Modulen und Unterstützung for solution-file-ähnliche ts-config.json
-Dateien. Alle weiteren Neuerungen und Details erfahrt ihr über den offiziellen Blogpost zum Release .
Und der awaited-Operator?
Wer den TypeScript-Issue-Tracker oder die Design-Meeting-Notes des Projekts verfolgt hat, kennt und erwartet den neuen awaited
-Operator vielleicht schon. Er soll es ermöglichen, Promise-Unwrapping in JS zukünftig genau modellieren zu können. Eine Veröffentlichung des Features war zwar für V 3.9 geplant, bei der Arbeit daran kam allerdings die Erkenntnis, dass das Feature zum jetzigen Zeitpunkt noch nicht ausgereift genug ist. Aus diesem Grund wurde der Operator vorerst aus dem Haupt-Branch entfernt und seine Veröffentlichung auf eine spätere Version verschoben.
- Vorgriff auf ECMAScript 2020: Das sind die spannendsten neuen Features in TypeScript 3.8
- Was ist eigentlich Typescript?
- Web-Development jetzt und später: 3 ½ Vorhersagen, die du lesen solltest