Vorgriff auf ECMAScript 2020: Das sind die spannendsten neuen Features in TypeScript 3.8
Type-only Im- und Exporte
Für das Referenzieren von Typen macht TypeScript Anleihen bei JavaScripts Import-Syntax. So war es bisher möglich, JavaScript-Values zusammen mit TypeScript-Typen zu importieren, etwa so:
// ./foo.ts
interface Options {
// ...
}
export function doThing(options: Options) {
//
}
// ./bar.ts
import { doThing, Options } from "./foo.js";
// doThing is a JS value, Options is purely a TypeScript type
function doThingBetter(options: Options) {
// do something twice as good
doThing(options);
doThing(options);
}
*Alle Code-Snippets wurden, soweit nicht anders gekennzeichnet, aus Michael Rosenwassers Blogpost für den Microsoft-Blog übernommen.
Das ist bequem, weil es meistens weniger eine Rolle spielt, ob ein Type oder Value importiert wird. Wichtig ist, überhaupt etwas zu importieren. In manchen Fällen ist es aber wichtig, zu unterscheiden, was importiert wird. TypeScripts Import Elision löscht Import Statements, deren Importe nur als Types genutzt werden. Das führt etwa bei Modulen mit Side-Effects zu abweichendem Verhalten – um Side Effects sicherzustellen, war bisher ein zweites Import Statement nötig.
Dieses Problem kam zum Beispiel in Angular in Version 1.x regelmäßig auf, wo Services global registriert werden mussten, diese aber überhaupt nur wegen der Types importiert wurden.
// ./service.ts
export class Service
{ // ...}
register("globalServiceId", Service);
// ./consumer.ts
import { Service } from "./service.js";
inject("globalServiceId", function (service: Service) {
// do stuff with Service
});
Im Beispiel wird ./service.js aufgrund diesen Verhaltens nie ausgeführt. Die Folge: Während der Laufzeit crasht die App.
Um derartige Probleme zu vermeiden, wurde in Version 3.8 eine Syntax für Type-only Im- und Exporte eingeführt. So sieht sie aus:
import type { SomeThing } from "./some-module.js";
export type { SomeThing };
Über import type
werden nur Deklarationen, die ausschließlich für Typen-Annotationen und Deklarationen genutzt werden, importiert. Ein solches Statement wird beim Kompilieren immer komplett gelöscht, sodass während der Laufzeit nichts mehr davon im Code vorhanden ist.
export type
kann nur für type-Kontexte genutzt werden und wird beim Kompilieren ebenfalls aus dem Output gelöscht.
Neue Compiler-Flag
In Version 3.8 wurde außerdem eine Compiler Flag hinzugefügt, über die ihr kontrollieren könnt, was aus Imports wird, die zur Laufzeit nicht verwendet werden. importsNotUsedAsValues
heißt sie. Die Flag nimmt drei verschiedene Werte an:
remove
: ist derzeit der Default und wird der Default bleiben – Importe, die nicht verwendet werden, werden gedroppt.
preserve
: führt zum Erhalt aller Importe, deren Values nicht verwendet werden. So können zum Beispiel side effects, wo Services nur wegen des Typs registriert werden, erhalten bleiben.
error
: Wie die preserve
-Flag erhält die error
-Flag alle Importe. Im Fall des Imports eines Values, der nur als Type verwendet wird, bekommt ihr mit dieser Flag allerdings eine Fehlermeldung. Das ist zum Beispiel nützlich, um sicherzustellen, dass ihr Values nicht aus Versehen importiert, Importe von Nebeneffekten aber trotzdem explizit machen wollt.
ECMAScript Private Fields
TypeScript 3.8 unterstützt die Private Fields des ECMAScript Stage-3 Class Fields Proposals. Private Fields schreibt ihr mit einem vorangestellten #
. Sie werden auch private names genannt.
class Person {
#name: string
constructor(name: string){
this.#name = name;
} greet() {
console.log(`Hello, my name is ${this.#name}!`); }}
let jeremy = new Person("Jeremy Bearimy"); jeremy.#name
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
Private Fields sind ausschließlich innerhalb der Klasse gültig, in der sie enthalten sind.
TypeScript-eigene Accessibility-Modifiers wie public
oder private
könnt ihr in Private Fields nicht verwenden. Und: Außerhalb der Klasse, in der sie enthalten sind, kann auf Private Fields weder zugegriffen noch können sie entdeckt werden. Das wird auch Hard Privacy genannt. Reguläre Property-Deklarationen kollidieren oft mit Klassennamen von irgendwelchen Unterklassen, mit Private Fields passiert das nicht – jeder Field Name betrifft nur die Klasse, in der er enthalten ist.
Wer das neue Feature in reinen JavaScript-Dateien nutzt: Diese müssen immer bestimmt werden, bevor sie einer Entität zugewiesen werden. In JavaScript war es bisher erlaubt, auf nicht deklarierte Properties zuzugreifen. Mit Private Fields ist das nicht möglich.
class C {
/** @type {number} */
#foo;
constructor(foo: number) {
// This works.
this.#foo = foo;
}
}
Ob ihr TypeScripts Private Modifiers nutzt oder die neuen
#
private fields aus ECMAScript, hängt vom jeweiligen Einsatzweck ab. TypeScripts private Modifiers werden, wenn sie auf Properties angewendet werden, komplett gelöscht. Die Daten sind zwar da, im kompilierten JavaScript-Output sind aber keine Informationen darüber, wie die Property deklariert wurde, hinterlegt – während der Laufzeit verhält sie sich wie eine normale Property. Das heißt, Privacy ist nur während der Kompilierzeit sichergestellt.
Diese „softe“ Privacy bietet Clients einen Workaround, zeitweise fehlenden Zugriff auf eine API auszugleichen. Und sie funktioniert in jeder Laufzeitumgebung. Auf ECMAScrips #privates
kann im Gegensatz dazu außerhalb der Klasse, in der sie definiert wurden, nicht zugegriffen werden. Deren strikte Privacy ist hilfreich, wenn ihr wirklich sicherstellen wollt, dass niemand auf Internals in eurem Code zugreift. Außerdem macht das Feature – wie bereits erwähnt – Sub-Classing einfacher, Kollisionen von Field-Benennungen sind damit ausgeschlossen – anders bei TypeScripts private
-Keyword.
Allerdings unterstützt TypeScript das Feature bisher nur bei ES6+ Targets. Im Gegensatz dazu funktioniert das private
-Keyword mit allen Targets ab ES3.
Neue export * -Syntax
Aus ECMAScript 2020 wurde in TypeScript 3.8 außerdem eine neue Syntax übernommen, mit der alle in einem Modul verwendeten Komponenten in einer Zeile exportiert werden können. Anstatt über
import * as utilities from "./utilities.js"
export { utilities }
alle Module einzeln zu exportieren, etwa so:
export { Accordion } from './Accordion';
export { Alert, AlertText } from './Alert';
export { AuthorCard } from './AuthorCard';
export { Avatar } from './Avatar';
export { Badge } from './Badge';
export { Box } from './Box';
export { Breadcrumbs, BreadcrumbsItem } from './Breadcrumbs';
Codebeispiel: t3n
, könnt ihr jetzt äquivalent zur Syntax des Import-Statements über
export * as utilities from "./utilities.js";
alle Module mit dieser einen Zeile exportieren – weniger Schreibarbeit, yay!
Top Level await
Die meisten Umgebungen mit Input/Output in JavaScript sind asynchron. Die meisten API geben Promises zurück. Das hat den Vorteil, dass Vorgänge parallel ausgeführt werden können. Beim Laden externer Inhalte ist diese Asynchronität aber unerwünscht.
fetch("...")
.then(response => response.text())
.then(greeting => { console.log(greeting) });
then
-Ketten wie im Beispiel sind ein Weg, Vorgänge trotzdem nacheinander auszuführen.
Um then
-Ketten zu vermeiden, nutzen JavaScript-Entwickler oft eine async
-Funktion, um über await
sicherzustellen, dass response
und greeting
nacheinander ausgeführt werden.
async function main() {
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);
}
main()
.catch(e => console.error(e))
Mit einem neuen Feature aus ECMAScript 2020 namens top Level await ist es in TypeScript 3.8 möglich, await
auf dem obersten Level eines Moduls zu nutzen, die async
-Funktion braucht es damit an dieser Stelle nicht mehr.
const response = await fetch ("...")
const greeting = await response.text();
console.log(greeting);
// Make sure we're a module
export {};
Top level await
funktioniert aber nur auf dem obersten Level eines Moduls. Dateien gelten nur dann als Module, wenn ein import
oder export
vorhanden ist. Um das sicherzustellen, müsst ihr in manchen Fällen wie im Beispiel export {}
ausschreiben.
Achtung: Im Moment funktioniert das Feature nur für Target Compiler Options ES17+.
es2020-Option für target und module
Um sicherzustellen, dass neue ECMAScript-Features wie die dynamische import(...)
-Syntax oder export * as ns
auch funktionieren, gibt es in V 3.8 die Option es2020
für target
und module
.
JSDoc Property Modifiers
Über eine neue Flag namens allowJS
könnt ihr in TypeScript 3.8 JavaScript-Dateien nutzen und sie über die checkJS-Option type-checken – alternativ könnt ihr euren JavaScript-Dateien auch // @ts-check
voranstellen. In JavaScript gibt es keine eigene Syntax für die Typüberprüfung. Um Type-Checking in JavaScript-Files zu ermöglichen, macht TypeScript 3.8 Anleihen bei JSDoc, es versteht die Accessibility-Modifier @public
, @private
und @protected
, die äquivalent zu public
, private
und protected
in TypeScript funktionieren. Außerdem neu ist der @readonly
-Modifier, der dafür sorgt, dass einer Property nur bei der Initialisierung ein Value zugewiesen werden kann.
Neue watch-Optionen für Directories
TypeScript 3.8 kommt mit einer neuen Strategie für die Überwachung von Directories, über die ihr Änderungen von Node-Modulen besser im Blick behalten könnt. Damit ihr das entsprechend der Anforderungen eurer einzelner Projekte individuell konfigurieren könnt, wurde mit TypeScript 3.8 ein watchOptions
-Field in euren tsconfig.json beziehungsweise jsconfig.json-Dateien eingeführt. Für die Konfiguration stehen euch vier verschiedene Optionen zur Auswahl; wer sich für die Details interessiert, sei auf den Microsoft-Blog oder auch auf den Pull-Request auf GitHub verwiesen.
„Fast & Loose“ Incremental Checking
TypeScript verfügt über zwei Modi, --watch
und --incremental
, über die TypeScript Dateien und deren Auswirkung auf andere Dateien trackt und Information im Speicher so weit wie möglich wiederverwendet.
Über eine neue Compiler-Option namens OnlyAffectDirectDependencies
werden nur jene Dateien gecheckt, die von einer Änderung betroffen sind, sowie jene, die diese Dateien importieren. Das bringt vor allem einen Performance-Zuwachs. Dieses Feature ist vielleicht nicht für alle Codebases empfehlenswert, wer nach dem „move fast & fix things later“-Prinzip arbeiten will, findet dafür aber eventuell Verwendung.
Außerdem kommt Version 3.8 mit einigen kleineren Breaking Changes, die vor allem strikteres Type Checking betreffen und neuen Editor-Features. Alle Details zu allen Neuerungen könnt ihr im Blogpost von Microsoft oder im Projekt auf GitHub nachlesen.
Das könnte dich auch interessieren:
- Web-Development jetzt und später: 3 1/2 Vorhersagen, die du lesen solltest
- TypeScript, Rust und Kotlin: Warum die neuen Programmiersprachen voll im Trend sind
- JavaScript: Das waren die beliebtesten Tools und Frameworks 2019