Eine Quelle, viele Typen

Typen ableiten, statt dieselben Felder dreimal zu pflegen

Pick, Omit, Partial und Record erzeugen aus einem Datentyp alle Varianten, die Formular, Anlage und Aktualisierung brauchen. Eine Falle gibt es dabei, und sie meldet sich nicht.

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

Drei Feldlisten für dieselbe Sache, und zwei davon sind veraltet

Ein Datentyp Kunde hat zwölf Felder. Daneben stehen im Projekt: ein Typ für das Anlageformular ohne die vom System vergebene Kennung, ein Typ für die Änderung, bei dem alles optional ist, und ein Typ für die Listenansicht mit vier Feldern. Wenn alle vier von Hand geschrieben sind, kommt beim nächsten neuen Feld eine Änderung an vier Stellen. Nachgetragen wird es an zweien, und der Compiler sagt nichts, weil aus seiner Sicht vier unabhängige Typen existieren, die einfach unterschiedlich aussehen.

Der Fehler zeigt sich später und an unerwarteter Stelle. Das Formular schickt ein Feld mit, das der Änderungstyp nicht kennt, oder die Listenansicht liest ein Feld, das nie geladen wird. Beim Suchen nach der Ursache sieht jeder einzelne Typ richtig aus, denn er ist ja in sich stimmig. Nur die Beziehung zwischen ihnen fehlt, und genau die kann man nicht sehen, wenn sie nirgends aufgeschrieben ist.

Der zweite Fall ist noch unauffälliger: Ein Objekt mit Voreinstellungen steht als Wert im Code, und daneben steht ein handgeschriebener Typ, der dieselben Felder beschreibt. Beide passen am ersten Tag zueinander. Sobald jemand eine Voreinstellung ergänzt, ohne den Typ anzufassen, beschreibt der Typ etwas, das es so nicht mehr gibt. Dabei wäre der Wert die bessere Quelle gewesen, denn er ist das, was tatsächlich läuft.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Aufbau im Detail

Der Aufbau im Detail

Der Aufbau

type Formularwerte = Partial<Pick<Kunde, 'name' | 'ort'>>
  1. 01 Der abgeleitete Typ bekommt einen eigenen Namen type Formularwerte

    Ein Name lohnt sich, sobald der Typ mehr als einmal gebraucht wird. Er taucht in Fehlermeldungen auf und macht sie damit lesbarer, als wenn dort die ausgeschriebene Form steht.

  2. 02 Macht jedes enthaltene Feld optional Partial

    Nach dieser Anwendung darf jedes Feld fehlen. Das ist für Formularzwischenstände und für Änderungen richtig und für Antworten aus dem Netz meist falsch, weil dort danach jeder Zugriff eine Prüfung auf undefined verlangt.

  3. 03 Wählt Felder aus einem bestehenden Typ Pick

    Die Auswahl wird gegen den Quelltyp geprüft. Ein Feld, das es in Kunde nicht gibt, führt sofort zu einer Meldung. Genau darin liegt der Unterschied zu Omit, das an derselben Stelle stillbleibt.

  4. 04 Die einzige Stelle, an der die Felder wirklich stehen Kunde

    Alles andere im Projekt zeigt hierher. Kommt ein Feld dazu, ändert sich diese eine Deklaration, und die abgeleiteten Typen ziehen automatisch mit. Das ist der ganze Zweck der Übung.

  5. 05 Die Auswahl als Vereinigung von Feldnamen 'name' | 'ort'

    Hier stehen Feldnamen als Typen, nicht als Zeichenketten im üblichen Sinn. Deshalb schlägt die Entwicklungsumgebung sie vor, und deshalb meldet ein Umbenennen im Quelltyp genau diese Zeile.

Wenn es nicht funktioniert

Das siehst du

Ein Feldname in Omit ist falsch geschrieben, und trotzdem meldet niemand etwas. Das Feld bleibt im abgeleiteten Typ erhalten.

Warum

Omit schränkt seinen zweiten Parameter nicht auf die Schlüssel des Quelltyps ein, sondern nimmt jede Zeichenkette entgegen. Ein Name, den es nicht gibt, entfernt eben nichts.

Was hilft

Nimm für kleine Auswahlen lieber Pick, weil dort geprüft wird. Wo Omit passt, definier einmal einen eigenen Hilfstyp, dessen zweiter Parameter auf keyof des Quelltyps eingeschränkt ist, und benutz im Projekt nur noch diesen.

Das siehst du

Nach einem Omit über einen Typ mit mehreren Varianten lässt sich das Ergebnis nicht mehr über das unterscheidende Feld auseinanderhalten.

Warum

Omit verteilt sich nicht über eine Vereinigung, sondern behandelt sie als ein Ganzes. Aus zwei Varianten wird dabei ein einziger Typ mit den gemeinsamen Feldern, und die Unterscheidung geht verloren.

Was hilft

Bau einen verteilenden Hilfstyp, der die Vereinigung über einen bedingten Typ auseinandernimmt und Omit auf jede Variante einzeln anwendet. Das sind zwei Zeilen, die einmal im Projekt stehen.

Das siehst du

Nach der Umstellung auf Partial steht in der halben Datei eine Meldung, dass ein Wert möglicherweise undefined ist.

Warum

Partial hat jedes Feld optional gemacht, auch die, die fachlich immer gefüllt sind. Der Compiler zieht daraus die richtigen Schlüsse, nur war die Absicht eine andere.

Was hilft

Kombinier statt pauschal zu lockern: Partial über die wirklich optionalen Felder, zusammengeführt mit Pick über die zwingenden. Damit bleibt die Kennung eine Kennung, und nur der Rest darf fehlen.

Das siehst du

Ein neuer Status kommt in den Datentyp, aber die Beschriftungstabelle bleibt unvollständig, ohne dass jemand eine Meldung sieht.

Warum

Die Tabelle ist als Record<string, string> deklariert. Damit ist jeder Schlüssel erlaubt, und Vollständigkeit verlangt niemand.

Was hilft

Setz den Schlüsseltyp auf die Vereinigung der tatsächlichen Werte, also Record<Status, string>. Ab dann meldet der Compiler den fehlenden Eintrag genau an der Stelle, an der die Tabelle steht.

Das siehst du

keyof auf einen Typ liefert unerwartet string und number statt der aufgezählten Feldnamen.

Warum

Der Typ enthält eine Indexsignatur. Damit ist jeder Name erlaubt, und keyof gibt folgerichtig den ganzen erlaubten Bereich zurück statt einer Liste.

Was hilft

Prüf, ob die Indexsignatur wirklich gebraucht wird. Oft ist sie nur da, weil irgendwann ein dynamischer Zugriff einfacher war. Wo sie bleiben muss, arbeitest du mit einer getrennt gepflegten Vereinigung der bekannten Namen.

Sechs Ableitungen, die fast jedes Projekt braucht

  1. 01 Pick wählt Felder aus und prüft dabei jeden Namen gegen den Quelltyp.
  2. 02 Omit lässt Felder weg, prüft die Namen aber nicht auf Existenz.
  3. 03 Partial macht jedes Feld optional, Required dreht das wieder um.
  4. 04 Record verlangt für jeden Wert einer Vereinigung genau einen Eintrag.
  5. 05 typeof holt den Typ aus einem Wert, der ohnehin im Code steht.
  6. 06 keyof liefert die Feldnamen, ohne dass du sie ein zweites Mal aufschreibst.
Was du mitnimmst

Was du danach nicht mehr doppelt aufschreibst

Die mitgelieferten Hilfstypen decken die immer gleichen Fälle ab: Felder auswählen, Felder weglassen, alles optional machen, eine Zuordnungstabelle beschreiben. Der Gewinn entsteht nicht durch die einzelne Zeile, sondern dadurch, dass es danach nur noch eine Stelle gibt, an der Felder stehen.

Die Quelle bewusst wählen

Du entscheidest, welcher Typ im Projekt die Wahrheit über die Felder enthält, und leitest alles Weitere daraus ab. Meist ist das der Datentyp, den auch die Schnittstelle liefert, und nicht der Typ, den eine Oberfläche gerade braucht.

Pick und Omit auseinanderhalten

Pick ist die sichere Richtung, weil die Auswahl gegen den Quelltyp geprüft wird. Omit prüft die Schlüssel nicht, ein Tippfehler bleibt dort folgenlos. Du weißt, wann du deshalb lieber die Auswahl aufzählst als das Weglassen.

Partial nur dort, wo wirklich alles optional ist

Für eine Änderung, bei der jedes Feld fehlen darf, ist Partial richtig. Wo die Kennung zwingend bleibt, kombinierst du es, also Partial<Kunde> zusammen mit der ausdrücklich verlangten Kennung, statt später überall auf undefined zu prüfen.

typeof für Typen aus vorhandenen Werten

Steht die Wahrheit als Objekt im Code, holst du dir den Typ mit typeof statt ihn nachzubauen. Zusammen mit as const bleiben dabei auch feste Zeichenketten erhalten, statt zu string zu verallgemeinern.

keyof statt einer zweiten Namensliste

Wo du Feldnamen brauchst, etwa für Spalten oder Formularfelder, nimmst du keyof auf den Quelltyp. Damit fällt ein umbenanntes Feld sofort auf, statt still eine leere Spalte zu erzeugen.

Record für vollständige Zuordnungen

Mit Record über eine Vereinigung fester Werte verlangt der Compiler einen Eintrag für jeden Fall. Ein neuer Status im Datentyp erzeugt damit sofort eine Meldung an der Stelle, an der die Beschriftung fehlt.

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

Pick und Omit sind nicht spiegelbildlich

Beide erzeugen aus einem Typ einen kleineren, und das ist auch schon die ganze Gemeinsamkeit. Pick verlangt, dass die angegebenen Namen tatsächlich im Quelltyp vorkommen, denn sein zweiter Parameter ist auf keyof des ersten eingeschränkt. Ein Tippfehler wird gemeldet, und die Entwicklungsumgebung schlägt beim Tippen die vorhandenen Felder vor. Omit dagegen nimmt jede Zeichenkette entgegen. Wer versehentlich 'kennnung' statt 'kennung' schreibt, entfernt nichts, bekommt keine Meldung und arbeitet ab diesem Moment mit einem Typ, der ein Feld enthält, das er nicht enthalten sollte.

Daraus folgt eine einfache Faustregel: Wenn die Auswahl kurz ist, nimm Pick. Wenn du aus zwölf Feldern eines entfernen willst, ist Omit natürlich kürzer, und dann lohnt sich ein eigener Hilfstyp im Projekt, dessen zweiter Parameter auf keyof eingeschränkt ist. Diese drei Zeilen schreibst du einmal, und ab dann meldet auch das Weglassen einen falschen Namen.

Die zweite Eigenheit von Omit betrifft Typen mit mehreren Varianten, also Vereinigungen, die über ein Feld unterschieden werden. Omit arbeitet nicht auf jeder Variante einzeln, sondern auf der Vereinigung als Ganzem, und übrig bleibt eine einzige Form mit den gemeinsamen Feldern. Wer danach über das unterscheidende Feld verzweigen will, stellt fest, dass der Compiler die Varianten nicht mehr auseinanderhalten kann. Der Ausweg ist ein verteilender Hilfstyp, der über einen bedingten Typ jede Variante einzeln behandelt.

typeof und as const: wenn der Wert die bessere Quelle ist

Manchmal ist die Wahrheit über eine Struktur kein Typ, sondern ein Objekt, das ohnehin im Code steht: die Voreinstellungen einer Komponente, eine Konfiguration, eine Liste erlaubter Werte. Dann ist es unnötig, den passenden Typ danebenzuschreiben, denn genau dieses Nebeneinander läuft auseinander. Mit type Konfiguration = typeof standardKonfiguration holst du den Typ aus dem Wert. Ändert sich der Wert, ändert sich der Typ mit, und zwar ohne dass irgendwer daran denken muss.

Ohne Zusatz verallgemeinert der Compiler dabei allerdings. Aus einem Feld mit dem Wert 'kompakt' wird string, denn ein Objektfeld ist veränderlich und könnte morgen etwas anderes enthalten. Mit as const hinter dem Objekt bleiben alle Felder unveränderlich und die Werte in ihrer engen Form erhalten. Diese Kombination ist die Grundlage für ein Muster, das häufig gebraucht wird: eine Liste erlaubter Werte als Konstante schreiben, mit as const festhalten und daraus über den Zugriff mit [number] den Vereinigungstyp gewinnen. Damit stehen die erlaubten Werte genau einmal im Projekt, und sie stehen dort, wo sie zur Laufzeit ohnehin gebraucht werden.

Zu diesem Muster gehört keyof als Gegenstück. Wo du Feldnamen brauchst, etwa um Spalten einer Tabelle oder Felder eines Formulars zu benennen, schreibst du keyof Kunde statt einer zweiten Liste mit denselben Namen. Der Unterschied zeigt sich beim Umbenennen: Mit keyof meldet der Compiler jede Stelle, ohne keyof bleibt eine Zeichenkette stehen, die zu nichts mehr passt und still eine leere Spalte erzeugt.

Ableiten aus Funktionen und Schemas

Neben Feldern lassen sich auch Signaturen als Quelle nutzen. ReturnType<typeof erzeugeSpeicher> liefert den Rückgabetyp einer Funktion, ohne dass du ihn benennen musst, Parameters<typeof f> die Parameter als Tupel, und Awaited<T> holt den Typ aus einem Promise heraus. Zusammengesetzt ergibt das Awaited<ReturnType<typeof ladeKunden>>, also den Typ dessen, was am Ende tatsächlich ankommt. Für Funktionen, deren Rückgabewert aus mehreren Schritten zusammengesetzt ist, spart das eine Deklaration, die sonst zwangsläufig hinterherhinkt.

Denselben Gedanken verfolgen Schemabibliotheken. Du beschreibst die Struktur einmal als Schema, bekommst daraus eine Prüfung zur Laufzeit und leitest den TypeScript-Typ aus demselben Schema ab. Damit ist ausgeschlossen, dass Prüfung und Typ auseinanderlaufen, denn beide stammen aus einer Zeile. Wo Daten aus dem Netz oder aus Formularen kommen, ist das die konsequenteste Form der Ableitung, weil sie über die Grenze der Übersetzungszeit hinausreicht.

Bei aller Ableitung gibt es eine Grenze, die man kennen sollte. Ein Typ, der über fünf Ecken aus etwas anderem entsteht, ist beim Lesen kaum noch zu erfassen, und in Fehlermeldungen erscheint er ausgeschrieben in voller Länge. Wenn du beim Hovern nicht mehr erkennst, welche Felder ein Typ hat, ist das der Punkt, an dem eine ausdrückliche Deklaration die bessere Wahl ist. Ableiten spart Pflege, aber es ersetzt nicht die Verständlichkeit.

Der Unterschied zwischen Weglassen und Verbieten

Eine Feinheit, die im Alltag Zeit kostet: Ein optionales Feld und ein Feld, das undefined enthalten darf, sind für TypeScript standardmäßig fast dasselbe. Partial macht aus name: string das Feld name?: string, und damit ist auch die ausdrückliche Übergabe von undefined erlaubt. In vielen Fällen ist das egal. Wo es nicht egal ist, etwa bei einer Änderung, bei der ein fehlendes Feld unverändert lässt und ein ausdrückliches undefined löschen soll, brauchst du die Option exactOptionalPropertyTypes, die genau diesen Unterschied wieder herstellt. Sie gehört nicht zu strict und muss einzeln eingeschaltet werden.

Verwandt ist die Frage, ob ein Zugriff über einen Index oder einen dynamischen Schlüssel undefined liefern kann. Standardmäßig behauptet der Compiler, dass hinter jedem Schlüssel ein Wert steht, obwohl das offensichtlich nicht stimmt. noUncheckedIndexedAccess ändert das und macht aus jedem solchen Zugriff eine Vereinigung mit undefined. In Code, der viel mit Nachschlagetabellen arbeitet, ist das die Option mit dem größten Ehrlichkeitsgewinn und gleichzeitig die mit den meisten neuen Meldungen. Auch sie ist bewusst nicht Teil von strict.

Dazu passende Kurse

Wer im eigenen Projekt Typen aus vorhandenen Typen ableiten will, findet die Muster dafür in den TypeScript-Kursen.

Weil die meisten dieser Ableitungen in Formularen landen, lohnt sich daneben ein Blick auf Kurse zu typisierten Formularen und 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

Wann leite ich ab und wann schreibe ich den Typ aus?
Leite ab, solange sich beim Lesen noch erkennen lässt, welche Felder herauskommen. Sobald ein Typ aus drei ineinandergeschachtelten Hilfstypen besteht und beim Hovern eine halbe Seite erscheint, ist die ausdrückliche Deklaration verständlicher. Der Zweck des Ableitens ist, dass Felder nur an einer Stelle stehen, nicht, möglichst wenige Zeilen zu schreiben.
Warum meldet Omit einen falschen Feldnamen nicht?
Weil sein zweiter Parameter nicht auf die Schlüssel des Quelltyps eingeschränkt ist, sondern auf alles, was als Schlüssel taugt. Ein Name, den es nicht gibt, entfernt schlicht nichts. Das ist bewusst so entworfen, damit sich Omit auch auf Typen anwenden lässt, deren Schlüssel nicht vollständig bekannt sind. Für den Alltag lohnt sich ein eigener Hilfstyp mit der strengeren Einschränkung.
Was bringt as const im Zusammenspiel mit typeof?
Ohne as const verallgemeinert der Compiler die Werte eines Objekts, aus 'kompakt' wird string und aus einer Liste ein Array mit veränderlichem Inhalt. Mit as const bleiben Werte und Reihenfolge in ihrer engen Form erhalten. Erst dadurch liefert typeof einen Typ, der die tatsächlichen Werte kennt, und genau darauf bauen Muster wie eine Vereinigung aus einer Liste erlaubter Werte auf.
Sind Record und eine Indexsignatur dasselbe?
Nicht ganz, und der Unterschied ist der wichtige. Record und eine Indexsignatur erlauben beide jeden Schlüssel. Record über eine Vereinigung fester Werte verlangt dagegen für jeden dieser Werte einen Eintrag, und ein fehlender wird gemeldet. Genau deshalb lohnt es sich, Beschriftungen und Zuordnungen über die tatsächliche Vereinigung zu deklarieren statt über string.
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.

Eine Feldliste im Projekt statt vier, die auseinanderlaufen

In den TypeScript-Kursen bei cmt leitest du Typen an eigenen Datenmodellen ab und siehst dabei, an welchen Stellen die mitgelieferten Hilfstypen still danebengreifen.