Tipps & Tutorials

TypeScript 5: Was ist neu?


Veröffentlicht am 26.04.2023 von Sebastian Springer

Mit TypeScript 5 ist die nächste Major-Version der auf JavaScript aufbauenden Programmiersprache erschienen. In der JavaScript- und TypeScript-Welt ist ein solches Major-Release in der Regel ein Ereignis, auf das viele EntwicklerInnen hinfiebern und andere zumindest Respekt davor haben. Doch keine Sorge: Das Team, das für TypeScript zuständig ist, ist sich seiner Verantwortung durchaus bewusst, und so halten sich die Breaking Changes sehr im Rahmen. 

Würde man Version 5 einen Namen geben, hätte er wahrscheinlich etwas mit Decorators zu tun, denn das ist wohl die größte Änderung in diesem Release. Doch dazu später mehr. 

Die wichtigsten Neuerungen in TypeScript 5

Der Fokus der Entwicklung von TypeScript liegt aktuell vor allem auf Performance und Developer Experience und weniger auf vielen neuen bahnbrechenden Features. Und so Fehler! Linkreferenz ungültig.haben die wichtigsten Punkte, die sich mit Version 5 ändern, mit der Build-Geschwindigkeit, dem Speicherverbrauch und der Größe von NPM-Paketen zu tun. 

Im Blogartikel zum Release gibt Daniel Rosenwasser beispielsweise an, dass die Buildzeit von VSCode mit TypeScript 5 80 Prozent im Vergleich zu Version 4.9 beträgt. Das heißt jetzt nicht, dass jedes TypeScript-Projekt plötzlich um 20 Prozent besser wird. Der Performancevorteil hängt vom Quellcode der Applikation ab. Eine wichtige Änderung ist der interne Umstieg von Namespaces auf Module. 

Was nicht nach viel klingt, führt zu beeindruckenden Zahlen: Der Compiler ist 10 bis 25 Prozent schneller. Das tsc-Kommando startet um 30 Prozent schneller, und das TypeScript-Paket ist um 26,4 MB von insgesamt 63,8 MB kleiner. TypeScript nutzt intern esbuild als Buildwerkzeug, was ebenfalls zu einem deutlichen Performancegewinn geführt hat. Den Umstieg auf ECMAScript-Module bezeichnet das TypeScript-Team als Änderung in der Infrastruktur der Programmiersprache.

Decorators – Metainformationen für Funktionen, Klassen und vieles mehr

Eine weitere, für EntwicklerInnen spürbare Änderung ist die Anpassung der Decorators von TypeScript. Als neue Major-Version ist es für das neue Release in Ordnung, auch mit bisherigen Schnittstellendefinitionen zu brechen. Und genau das macht TypeScript hier. 

Normalerweise geht das TypeScript-Team sehr behutsam bei größeren Änderungen vor. Im Fall der Decorators scheint dies nicht der Fall zu sein. Der Grund hierfür liegt in der Art des Decorator-Features, das über lange Zeit als experimentelles Feature gekennzeichnet war. Wer es benutzen wollte, musste die Eigenschaft „experimentalDecorators“ manuell aktivieren. Dabei gibt es zahlreiche Bibliotheken, die das Feature bereits produktiv nutzen. Beispiele hierfür sind Angular, TypeORM oder Nest. 

Den TypeScript-Decorators liegt das ECMAScript Proposal für Decorators zugrunde. Ursprünglich basieren die TypeScript Decorators auf dem Stage 2 Proposal. Mittlerweile haben die ECMAScript Decorators Stage 3 erreicht. Mit diesem Update hat sich auch die Schnittstelle der Decorators deutlich geändert. TypeScript trägt dieser Entwicklung Rechnung und passt die Schnittstelle an, sodass sie mit den Stage 3 Decorators kompatibel ist.

Generell sind Decorators Funktionen, die Klassen, Methoden, Eigenschaften oder Parameter mit Metainformationen versehen. Decorators eignen sich vor allem, um allgemeine Themen aus dem Quellcode auszulagern. Sehr populär sind sie bei Bibliotheken, um Strukturen zu konfigurieren. Ein Beispiel ist der @Column-Decorator von TypeORM, mit dem die Bibliothek dafür sorgt, dass eine Eigenschaft einer Entitätsklasse auf eine Spalte in einer Datenbanktabelle gemappt wird. Nest nutzt Decorators wie @Get oder @Post, um in einer Controller-Klasse Methoden zu Endpunkten einer API zu machen.

Als Beispiel für einen Decorator sehen Sie im Folgenden einen einfachen Logger, der vor und nach einem Methodenaufruf eine Ausgabe auf der Konsole erzeugt. Unabhängig von der TypeScript-Version können Sie, wie in Listing 1 zu sehen ist, eine Klasse Person mit einer Methode „getFullname“ definieren. 

class Person {
  constructor(private readonly firstname: string, private readonly lastname: string) { }
  @log
  getFullname(): string {
    return `${this.firstname} ${this.lastname}`;
  }
}

const p = new Person('Maria', 'Meier');
const fn = p.getFullname();
console.log(fn);

Diese Methode können Sie, unabhängig von der TypeScript-Version, mit dem @log-Decorator versehen.

Bei der Implementierung unterscheiden sich die Decorators bis Version 4 und ab Version 5 deutlich voneinander. In Version 4 arbeiten Sie in diesem Fall mit dem PropertyDescriptor und weisen seinem Value einen neuen Wert zu. Diese Wrapper-Funktion kümmert sich um das Logging, ruft die eigentliche Funktion auf und gibt das Ergebnis zurück.

 

function log() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      console.log('calling method');
      const result = originalMethod.call(this, ...args);
      console.log('after call');
      return result;
    }
  }
}

In TypeScript 5 haben Sie dagegen direkten Zugriff auf die Originalmethode und können sie in der inneren Funktion aufrufen, loggen und den Wert zurückgeben.

function log(originalMethod: any, _context: any) {
  return function (this: any, ...args: any[]) {
    console.log('calling method');
    const result = originalMethod.call(this, ...args);
    console.log('after call');
    return result;
  }
}

Mit dieser Anpassung rücken JavaScript und TypeScript wieder ein Stück näher zusammen. Die Idee dahinter ist, dass, wenn ein Feature, in diesem Fall die Decorators, nativ in JavaScript verfügbar ist, TypeScript keine weiteren Code-Transformationen mehr durchführen muss. Bei vielen modernen JavaScript-Features ist dies bereits der Fall, sodass JavaScript- und TypeScript-Code bis auf die Typangaben sehr ähnlich aussehen.

Const Type Parameters

Decorators haben einen direkten Einfluss auf den Quellcode und die Applikation. Im Gegensatz dazu zielt das nächste Feature, die Const Type Parameters, eher auf die Developer Experience und die Werkzeugunterstützung. 

Nehmen Sie eine einfache Funktion, wie die folgende extractAddress, die aus einem Objekt die Eigenschaft address extrahiert und zurückgibt.

type ThingWithAddress = {
  readonly address: {
    street: string;
    city: string;
  }
}

function extractAddress<T extends ThingWithAddress>(thingWAddr: T): T['address'] {
  return thingWAddr.address;
}

const address = extractAddress({ name: 'Lisa', address: { street: 'somestreet', city: 'somecity' } })

Die Type Inference von TypeScript nimmt ohne weiteres Zutun an, dass es sich bei den Eigenschaften street und city des resultierenden Objekts um Strings handelt, obwohl sie als readonly gekennzeichnet sind. An dieser Stelle wäre es eigentlich schön, wenn die tatsächlichen Werte, also „somestreet“ und „somecity“, verwendet werden würden. Das können Sie erreichen, wenn Sie beim Aufruf der extractAddress-Funktion „as const“ einfügen. Mit den Const Type Parameters können Sie das Gleiche aber auch auf Funktionsebene und nicht erst beim Aufruf erreichen, indem Sie dem generischen Typ T ein const voranstellen, wie im folgenden Listing.

type ThingWithAddress = {
  readonly address: {
    street: string;
    city: string;
  }
}

function extractAddress<const T extends ThingWithAddress>(thingWAddr: T): T['address'] {
  return thingWAddr.address;
}

const address = extractAddress({ name: 'Lisa', address: { street: 'somestreet', city: 'somecity' } })

 

Mit diesem Feature können Sie sich das „as const“ sparen und vermeiden so, dass EntwicklerInnen dies beim Aufruf der Funktion vergessen.

Enums als Union Types

Enums sind in TypeScript im weitesten Sinn mit Sammlungen von konstanten Werten vergleichbar. So können Sie beispielsweise ein Enum Colors definieren und hier die Werte red, blue und green definieren. Auf diese Werte können Sie dann innerhalb Ihrer Applikation über beispielsweise Colors.red zugreifen.

In TypeScript existieren zwei verschiedene Arten von Enums: numerische und literale. Bis zur Version 2 von TypeScript existierten lediglich die numerischen Enums, die jeden ihrer Einträge auf einen Zahlenwert abbilden. Die neueren literalen Enums ermöglichen es Ihnen, literale Werte, also beispielsweise Strings, zu verwenden. Jedes Element eines solchen Enums ist ein gültiger Typ für sich. Das Enum selbst ist ein Union Type seiner Elementtypen. 

Dieses System stößt jedoch an seine Grenzen, wenn der Wert durch einen Funktionsaufruf berechnet wird. TypeScript 5 löst dieses Problem, indem auch solche berechneten Werte einen eigenen Typ erhalten. Vor der Integration des neuen Enum-Features in Version 5 ist TypeScript auf den Standard der numerischen Enums zurückgegangen, was dafür gesorgt hat, dass Sie einige der Vorteile der Union Enums verlieren. So können Sie beispielsweise in Typescript 4 die Eigenschaften eines Enums mit einem berechneten Wert nicht als Typ für einen Funktionsparameter verwenden. 

Weitere Informationen zum Thema Enums finden Sie im TypeScript-Handbuch.

Support für Typ-Exporte

TypeScript entwickelt sich ständig weiter und die EntwicklerInnen ergänzen kontinuierlich neue Features. Manche davon erscheinen auch auf den zweiten Blick noch nicht vollständig oder inkonsistent. Hier bessern die nachfolgenden Releases häufig nach und beheben die Probleme, beziehungsweise fügen noch zusätzliche Features hinzu. 

Ein Beispiel hierfür sind die Type-Only Exporte, mit denen Sie Typinformationen aus einer Datei exportieren oder sogar über eine weitere Ebene re-exportieren können. TypeScript 5 fügt die beiden Statements „export type * from ‚module‘“ und „export type * as ns from ‚module‘“ hinzu, um die Type-Only Exporte zu komplettieren.

Editor-Unterstützung

TypeScript bietet Sicherheit bei der Entwicklung, indem es eine statische Typüberprüfung durchführt und EntwicklerInnen auf potenzielle Fehler aufmerksam macht. Darüber hinaus liefert TypeScript durch seine impliziten und expliziten Typinformationen den Entwicklungswerkzeugen und allen voran den Entwicklungsumgebungen wertvolle Hinweise, um EntwicklerInnen bei der Arbeit zu unterstützen. Ein prominentes Beispiel dafür ist die Autovervollständigung von Quellcode. 

Mit TypeScript 5 haben zwei weitere Features in den Sprachkern Einzug gehalten, die die Developer Experience noch weiter verbessern. So ist TypeScript in der Lage, Import-Statements unabhängig von Groß- und Kleinschreibung zu sortieren. 

Außerdem können Entwicklungsumgebungen mit TypeScript-Unterstützung jetzt switch-case-Statements um Optionen vervollständigen, wenn der überprüfte Typ einen Literal-Typ aufweist.

Erweiterung der JSDoc-Annotationen

Üblicherweise nutzen Sie TypeScript in seiner eigentlichen Form, also mit den Typangaben direkt im Quellcode. Das führt dazu, dass Sie den Quellcode vom Compiler übersetzen lassen müssen, damit Sie ihn im Browser oder in Node.js ausführen können. Der TypeScript Compiler unterstützt jedoch eine Reihe von Annotationen, die Sie in Form von JSDoc angeben können, um Typsicherheit für Ihren Code zu erreichen. Beispiele hierfür sind die @type-Annotation, mit der Sie den Typ einer Variablen angeben können. In Listing 6 sehen Sie einige Beispiele für den Einsatz von JSDoc im Zusammenspiel mit TypeScript.

 

/**
 * @type {string}
 */
let s;

Diese Variante von TypeScript ist keine neue Erfindung von Version 5, sondern existiert bereits seit Version 2.3. Neu in Version 5 sind die beiden Annotationen @satisfies und @overload. @satiesfies zeigt an, dass eine Datenstruktur mit einem bestimmten Typ kompatibel ist, ohne die Struktur in diesen Typ zu casten. Mit @overload können Sie angeben, dass eine Funktion mehrere Signaturen aufweist, also überladen ist. 

In TypeScript erreichen Sie die Überladung, indem Sie mehrere Funktionsdeklarationen implementieren und insgesamt eine konkrete Umsetzung dafür zur Verfügung stellen. Das ist in JavaScript nicht möglich, also müssen Sie hier auf das @overload-Feature zurückgreifen.

Konfiguration

Auch im Bereich der Konfiguration und der Optionen, die Sie dem Compiler übergeben können, hat sich einiges mit dem neuen Release getan. 

Sie können in der tsconfig.json-Datei mittlerweile über die extends-Eigenschaft mehrere Konfigurationsdateien angeben, von denen Sie erben möchten. Widersprechen sich die Konfigurationsoptionen, gewinnt die Datei, die später genannt wird.

Unter der Featurebezeichnung Resolution Customization Flags fasst TypeScript verschiedene Optionen wie beispielsweise --allowImportingTsExtensions zusammen, mit denen Sie das Verhalten des TypeScript-Compilers bei Imports und Exports, beziehungsweise die Auflösung von Dateien beeinflussen können.

Mit der –verbatimModuleSyntax-Option sorgt TypeScript dafür, dass alle Im- und Exporte mit einem Type-Modifikator in der Ausgabe des Compilers komplett verworfen werden, um das Resultat so klein wie möglich zu halten.

Mit der „moduleResolution: bundler“-Option trägt Typescript der Entwicklung moderner Bundler Rechnung. 

Außerdem gibt es eine Reihe von zusätzlichen Optionen, um die Buildartefakte des TypeScript Compilers besser zu steuern. So kann beispielsweise mit –sourceMap die Erzeugung einer SourceMap angestoßen werden. Damit können unterschiedliche Builds mit verschiedenen Artefakt-Konstellationen konfiguriert werden. 

Fazit – Was ist neu in TypeScript 5?

Das fünfte Major-Release macht TypeScript besser, schneller und kleiner. Vom umfangreichsten Feature, der internen Umstellung auf Module, bekommen Sie in der Regel kaum etwas mit und doch modernisiert es die Programmiersprache von Grund auf. Einige wenige Features wie beispielsweise die Decorators wirken sich tatsächlich auf den Alltag von EntwicklerInnen aus. Viele Neuerungen betreffen die Developer Experience und die Konfigurierbarkeit des Compilers.

Mit Version 5 bleibt sich TypeScript treu und bricht nicht mit der Vergangenheit, sondern macht die Programmiersprache zu einer noch besseren Version von JavaScript.

Bild von Markus Spiske auf Pixabay 

 

Der Autor:


Sebastian Springer

Sebastian Springer ist JavaScript-Entwickler bei Maiborn Wolff in München und beschäftigt sich vor allem mit der Architektur von client- und serverseitigem JavaScript. Er ist Berater und Dozent für JavaScript und vermittelt sein Wissen regelmäßig auf nationalen und internationalen Konferenzen.