Umstieg in Etappen

TypeScript einführen, ohne das Projekt anzuhalten

Beide Dateiarten laufen eine Zeit lang nebeneinander. Entscheidend sind die Reihenfolge der Module, der Umgang mit untypisierten Bibliotheken und eine Zahl, die den Fortschritt zeigt.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Der Umstieg scheitert an der Reihenfolge, nicht an der Sprache

Der naheliegende Plan ist ein eigener Zweig, in dem alles umbenannt und typisiert wird. Er scheitert zuverlässig, weil parallel weiterentwickelt wird und der Zweig mit jeder Woche schwerer zusammenzuführen ist. Nach sechs Wochen ist die Zusammenführung eine eigene Aufgabe mit eigenem Risiko, und irgendwann entscheidet jemand, dass sich das nicht mehr lohnt. Umgestellte Projekte sehen anders aus: Dort liegt eine Mischung aus .js und .ts über Monate im selben Verzeichnis, und niemand merkt es beim Arbeiten.

Die zweite typische Fehlentscheidung ist der Einstieg an der falschen Stelle. Wer mit einer Seitenkomponente anfängt, zieht deren komplette Importkette mit hinein und schreibt am ersten Tag Typen für zwanzig Module, die er noch nicht kennt. Wer mit einer Datumsfunktion ohne eigene Importe anfängt, ist in einer Viertelstunde fertig und hat gleich einen Nutzen: Jedes Modul, das diese Funktion später importiert, bekommt beim Umstellen bereits echte Typen geliefert statt Vermutungen.

Teuer ist außerdem, den ersten Tag mit strict zu beginnen. Dann prüfst du gleichzeitig zwei Dinge: ob die Umstellung technisch läuft und ob dein Code strengen Regeln genügt. Wenn dabei etwas hakt, weiß niemand, woran es liegt. Die Verschärfung ist ein eigener Vorgang und gehört hinter die Umstellung, nicht davor.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Schritt für Schritt

Schritt für Schritt

  1. 1

    Den Compiler danebenstellen, ohne etwas umzubenennen

    Leg eine tsconfig.json mit allowJs, ausgeschaltetem checkJs, strict auf false und skipLibCheck an. Nimm noEmit, wenn ein Bündler die Ausgabe erzeugt, und häng tsc als eigenen Prüfschritt in die Pipeline.

    Geschafft, wenn: tsc läuft durch und meldet nichts, obwohl noch keine einzige Datei .ts heißt.

  2. 2

    Typen für fremde Bibliotheken beschaffen

    Geh die Abhängigkeiten durch: eigene Typen, Typpaket unter @types oder gar nichts. Für den letzten Fall legst du eine eigene Deklarationsdatei mit den tatsächlich benutzten Funktionen an, statt das ganze Paket per Kurzform zu any zu machen.

    Geschafft, wenn: Kein Import ist mehr rot, und die verbliebenen Notdeklarationen stehen alle in einer Datei.

  3. 3

    Die Reihenfolge festlegen

    Bestimme die Module ohne eigene interne Importe, also Hilfsfunktionen, Formatierer und Datenmodelle. Das ist die erste Runde. Alles Weitere ergibt sich aus dem Abhängigkeitsbaum von unten nach oben.

    Geschafft, wenn: Es liegt eine geordnete Liste vor, an der sich mehrere Leute gleichzeitig bedienen können.

  4. 4

    Umbenennen und nur die Signaturen typisieren

    Benenn die Datei um und gib den exportierten Funktionen vollständige Signaturen sowie dem zentralen Datentyp eine eigene Deklaration. Was im Rumpf noch grob bleibt, bleibt zunächst grob.

    Geschafft, wenn: Die aufrufenden Module bekommen echte Vorschläge, obwohl im Inneren noch geraten wird.

  5. 5

    Offene Stellen markieren statt reparieren

    Setz @ts-expect-error mit einer Zeile Begründung und einer Vorgangsnummer, wo eine saubere Typisierung mehr als eine halbe Stunde kosten würde. Nimm nie @ts-ignore, weil das nie wieder auffällt.

    Geschafft, wenn: Jede Markierung meldet sich von selbst als überflüssig, sobald ihre Ursache behoben ist.

  6. 6

    Rückschritte technisch verhindern

    Bau eine Prüfung in die Pipeline, die neue .js-Dateien in den umgestellten Verzeichnissen ablehnt. Nimm zusätzlich die Regel auf, dass eine ohnehin angefasste .js-Datei bei der Gelegenheit umbenannt wird.

    Geschafft, wenn: Ein Pull Request mit einer neuen .js-Datei fällt in der Prüfung durch, ohne dass jemand es anmerken muss.

  7. 7

    Erst danach verschärfen

    Wenn nichts mehr .js heißt, schaltest du allowJs wieder ab, damit keine ungeprüfte Datei zurückkommt, und ziehst die Einzelprüfungen aus strict nacheinander hoch, beginnend mit noImplicitAny. strictNullChecks bekommt einen eigenen Termin.

    Geschafft, wenn: Jede neue Meldung lässt sich eindeutig der Verschärfung zuordnen und nicht mehr der Umstellung.

Sechs Etappen von der ersten Datei bis zum Ende

  1. 01 allowJs einschalten, damit .js und .ts im selben Projekt übersetzt werden.
  2. 02 Typpakete nachziehen und fehlende Deklarationen an einer Stelle sammeln.
  3. 03 Bei Modulen ohne eigene Importe anfangen und nach oben arbeiten.
  4. 04 Beim Umbenennen zuerst die nach außen sichtbaren Signaturen typisieren.
  5. 05 Offene Stellen mit @ts-expect-error markieren, damit sie sich selbst melden.
  6. 06 Neue .js-Dateien im Build sperren und den Anteil je Woche festhalten.
Was du mitnimmst

Was du danach sicher entscheidest

Der Umstieg besteht aus ungefähr einem Dutzend Entscheidungen, die alle einmal getroffen und dann durchgehalten werden. Wenn diese Entscheidungen stehen und aufgeschrieben sind, ist der Rest eine Aufgabe, die sich im Team verteilen lässt.

Beide Dateiarten nebeneinander betreiben

Du setzt allowJs, lässt checkJs zunächst aus und übersetzt damit ein gemischtes Projekt. Ab diesem Moment ist jede einzelne Umbenennung eine kleine Änderung, die für sich zusammengeführt werden kann.

Die Reihenfolge aus dem Abhängigkeitsbaum ableiten

Du fängst bei Modulen ohne eigene interne Importe an und arbeitest dich nach oben. Damit stehen die Typen der Importe fest, bevor du das nächste Modul anfasst, statt sie unterwegs zu erfinden.

Fremde Bibliotheken einordnen

Für jede Abhängigkeit klärst du: eigene Typen mitgeliefert, Typpaket unter @types vorhanden, oder gar nichts. Nur der dritte Fall kostet Arbeit, und dafür schreibst du eine eigene Deklarationsdatei statt einer Kurzform, die alles zu any macht.

Zuerst die Signaturen, dann die Innereien

Beim Umbenennen typisierst du die nach außen sichtbaren Funktionen und den Datentyp, mit dem das Modul arbeitet. Der Rumpf darf zunächst gröber bleiben, denn den Nutzen haben die Aufrufer.

JSDoc als Zwischenstufe nutzen

Wo eine Umbenennung gerade nicht passt, kannst du mit checkJs und Typangaben im Kommentar prüfen lassen, ohne die Datei anzufassen. Der Compiler versteht diese Angaben und meldet dieselben Fehler wie in einer .ts-Datei.

Rückschritte technisch ausschließen

Du sperrst neue .js-Dateien über eine Prüfung im Build. Ohne diese Sperre wächst der alte Bestand parallel weiter, und der Anteil bewegt sich trotz Arbeit kaum.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Die erste Konfiguration ist bewusst milde

Am Anfang steht eine tsconfig.json, die möglichst wenig verlangt: allowJs auf true, checkJs auf false, strict auf false und skipLibCheck auf true. Dazu noEmit, wenn ohnehin ein Bündelwerkzeug die Ausgabe erzeugt, denn dann ist tsc reiner Prüfer. Ziel dieses ersten Schritts ist eine einzige Beobachtung: tsc läuft durch und meldet nichts, obwohl noch keine Datei umbenannt wurde. Erst wenn das steht, fängt die eigentliche Arbeit an.

Zwei Einstellungen ersparen später Ärger. moduleResolution gehört auf den Wert, der zu eurem Werkzeug passt, für Bündler ist das bundler. Und esModuleInterop auf true nimmt den Streit darum weg, ob ein Paket im alten CommonJS-Format mit einer Standardeinfuhr importiert werden darf. Ohne diese Einstellung bekommst du an genau den Stellen Meldungen, an denen der Code seit Jahren einwandfrei läuft, was den Eindruck erweckt, TypeScript liege falsch.

skipLibCheck verdient einen Hinweis. Es nimmt alle Deklarationsdateien aus der Prüfung, also auch eure eigenen. Für die Umstellungsphase ist das richtig, weil sonst ein Widerspruch zwischen zwei Versionen fremder Typen den ganzen Lauf blockiert. Nach der Umstellung gehört die Einstellung noch einmal auf den Prüfstand, spätestens dann, wenn ihr selbst .d.ts-Dateien pflegt.

Fremde Bibliotheken: drei Fälle und ein Sonderfall

Der erste Fall ist der einfachste: Das Paket bringt eigene Typen mit, erkennbar an einem types- oder typings-Eintrag in seiner package.json. Da ist nichts zu tun. Der zweite Fall ist ein Typpaket aus dem DefinitelyTyped-Bestand, also @types/paketname als Entwicklungsabhängigkeit. Achte darauf, dass dessen Hauptversion zur Bibliothek passt, sonst beschreiben die Typen eine andere Schnittstelle als die, die tatsächlich läuft. Diese Kombination erzeugt Meldungen, die niemand nachvollziehen kann, weil der Code stimmt und die Beschreibung nicht.

Der dritte Fall ist die Bibliothek ohne jede Typinformation. Hier gibt es zwei Wege, und der bequeme ist der falsche: declare module 'paketname'; in einer .d.ts-Datei bringt den Compiler zum Schweigen und macht den gesamten Inhalt des Pakets zu any. Der bessere Weg kostet zwanzig Minuten: eine eigene Deklarationsdatei, in der genau die Funktionen stehen, die ihr tatsächlich benutzt. Das sind fast immer drei bis fünf. Alles, was ihr nicht aufruft, muss auch nicht beschrieben werden.

Der Sonderfall betrifft globale Werte, die zur Laufzeit von außen gesetzt werden, etwa eine Konfiguration im HTML oder ein eingebundenes Analysewerkzeug. Dafür legst du eine Datei mit declare global an und beschreibst darin die Erweiterung des Window-Objekts. Ohne diese Beschreibung landen genau diese Zugriffe erfahrungsgemäß als Zusicherungen im Code, und dann sind sie über das Projekt verteilt statt an einer Stelle.

Die Reihenfolge und was beim Umbenennen wirklich passiert

Typen fließen entlang der Importe. Wenn ein Modul eine Funktion aus einem anderen holt, kennt es deren Typen genau dann, wenn das andere Modul bereits umgestellt ist. Daraus folgt die Reihenfolge: erst die Blätter, also Hilfsfunktionen, Datenmodelle, Formatierer, Konstanten. Danach die Schicht darüber, und zum Schluss die Komponenten und Einstiegspunkte. Wer umgekehrt vorgeht, schreibt beim ersten Modul Typen für alles, was darunter liegt, und wirft sie später wieder weg.

Beim eigentlichen Umbenennen entstehen fast immer dieselben Meldungen: fehlende Typen an Funktionsparametern, Zugriffe auf Objekte ohne beschriebene Form, und Importe, die nun eine Endung oder eine andere Schreibweise verlangen. Arbeite dabei von außen nach innen. Zuerst bekommen die exportierten Funktionen eine vollständige Signatur, dann der Datentyp, mit dem das Modul umgeht. Der Rumpf darf zunächst gröber typisiert bleiben, denn der Nutzen liegt bei den Aufrufern und nicht in der Datei selbst.

Wenn eine Stelle sich in vertretbarer Zeit nicht sauber typisieren lässt, markierst du sie mit @ts-expect-error und einer Zeile Begründung. Der Unterschied zu @ts-ignore ist entscheidend: Sobald die Ursache behoben ist, meldet der Compiler die Markierung als überflüssig, und sie verschwindet. Damit räumt sich diese Art von Schulden von selbst wieder ab, statt jahrelang stehen zu bleiben.

Fortschritt messen, damit die Umstellung nicht versandet

Zwei Zahlen reichen. Die erste ist der Anteil der .ts-Dateien an allen Quelldateien, ermittelt mit einem Zweizeiler in der Pipeline. Die zweite ist der Anteil der Ausdrücke mit echtem Typ, für den es das Werkzeug type-coverage gibt. Die erste Zahl zeigt, wie weit die Umbenennung ist, die zweite, wie viel davon tatsächlich geprüft wird. Beide zusammen verhindern den Zustand, in dem alles .ts heißt und trotzdem nichts geprüft ist, weil überall any steht.

Damit die Zahlen sich bewegen können, muss der alte Bestand aufhören zu wachsen. Eine Prüfung im Build, die neue .js-Dateien in bestimmten Verzeichnissen ablehnt, kostet fünf Zeilen und ersetzt jede Ermahnung im Review. Sinnvoll ist außerdem eine Regel für Pull Requests: Wer eine .js-Datei ohnehin anfasst, benennt sie bei der Gelegenheit um. So wandert die Umstellung dorthin, wo tatsächlich gearbeitet wird, und die selten berührten Dateien bleiben liegen, was in Ordnung ist.

Zum Schluss die Verschärfung, und zwar als eigener Vorgang. Erst wenn nichts mehr .js heißt, ergibt es Sinn, allowJs wieder abzuschalten und die einzelnen Prüfungen aus strict nacheinander einzuschalten. Wer beides gleichzeitig macht, kann bei jeder Meldung nicht mehr unterscheiden, ob sie aus der Umstellung stammt oder aus der neuen Strenge, und verliert damit den einzigen verlässlichen Hinweis darauf, wo er gerade steht.

Dazu passende Kurse

Wer den Umstieg in Etappen statt großem Umbau plant, findet die einzelnen Schritte in den TypeScript-Kursen wieder.

Weil die Komponenten meist zuletzt an der Reihe sind, lohnt sich für diesen Teil ein Blick auf Kurse zu React mit typisierten Komponenten .

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Ein sehr gutes, praxisorientiertes und nachhaltiges Seminar. So sollte es immer sein. Vielen Dank.
LimeSurvey - Anwendertraining Teil 1 (Grundlagen)
Die Schulung ist gerade für Einsteiger oder Entwickler*innen mit eingestaubtem Basiswissen sehr hilfreich.
Vue.js 3 Grundkurs
Ich bin sehr glücklich Franz als Trainer gehabt zu haben. Er hat offensichtlich unglaublich viel Wissen zu den Themen, und schafft es, dieses auch interaktiv und verständlich weiterzugeben.
React Komplettausbildung

Häufige Fragen

Kann ich .js und .ts wirklich dauerhaft mischen?
Technisch ja, mit allowJs übersetzt TypeScript beides im selben Projekt. Als Dauerzustand ist es trotzdem unglücklich, weil beim Lesen einer Datei nie klar ist, welche Garantien gerade gelten. Sinnvoll ist die Mischung als Übergang mit einem groben Enddatum. Wenn der Anteil ein Jahr lang bei sechzig Prozent steht, fehlt keine Technik, sondern eine Entscheidung.
Was ist mit Dateien, die niemand mehr anfasst?
Die dürfen liegen bleiben. Der Nutzen der Typisierung entsteht beim Ändern und beim Lesen, und beides passiert dort nicht. Wichtig ist nur, dass diese Dateien nicht mitten in der Importkette eines umgestellten Moduls sitzen, denn dann liefern sie ungeprüfte Werte in den bereits sauberen Bereich. Ist das der Fall, reicht oft eine Deklarationsdatei daneben, die ihre nach außen sichtbaren Funktionen beschreibt.
Lohnt sich ein Werkzeug, das die Umstellung automatisch macht?
Für das reine Umbenennen und das Einfügen von Markierungen sparen solche Werkzeuge Zeit, und ihr Ergebnis ist ein Ausgangspunkt, kein Ziel. Was sie nicht leisten können, ist die eigentliche Arbeit: zu entscheiden, welche Form die Daten deines Systems haben. Rechne damit, dass ein automatischer Lauf viele Markierungen hinterlässt, die du danach einzeln durchgehst. Das ist in Ordnung, solange die Markierungen sich selbst melden, wenn sie überflüssig werden.
Sollen wir gleich mit strict anfangen?
Bei einem neuen Projekt unbedingt, im Bestand nicht. Während der Umstellung prüfst du sonst zwei Dinge gleichzeitig und kannst bei einer Meldung nicht mehr sagen, ob sie aus der Umbenennung oder aus der Strenge kommt. Setz strict für neu angelegte Verzeichnisse über eine zweite Konfiguration, und zieh den Rest nach, wenn die Umbenennung durch ist.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Webentwicklung-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Vom gemischten Verzeichnis zu einem Projekt mit echten Typen

In den TypeScript-Kursen bei cmt gehst du die Umstellung an vorhandenem Code durch, von der ersten Konfiguration bis zu den Typen für Bibliotheken, die keine mitbringen.