Warum es eine schlechte Idee ist ein Union-Typ zu einem Tuple zu konvertieren

TypeScript bietet eine Vielzahl nützlicher Typannotationen, um die Typsicherheit zu gewährleisten und die Codequalität zu verbessern. Zwei dieser Annotationen sind Union-Typen und Tupel, die häufig in TypeScript-Projekten verwendet werden. Obwohl Unions und Tupel ihre eigenen spezifischen Anwendungsfälle haben, haben einige Entwickler versucht, einen Union-Typ in ein Tupel zu konvertieren. Diese Praxis kann jedoch mehrere negative Konsequenzen haben und wird generell nicht empfohlen. In diesem Artikel erklären wir, warum die Umwandlung eines Union-Typs in ein Tupel eine schlechte Idee ist.

In TypeScript repräsentiert ein Union-Typ einen Wert, der einer von mehreren Typen sein kann. Im Gegensatz dazu ist ein Tupel eine fest geordnete Liste von Elementen. Obwohl es verlockend sein mag, einen Union-Typ in ein Tupel umzuwandeln, kann dieser Ansatz zu verschiedenen Problemen führen. In diesem Beitrag werden die Herausforderungen bei der Umwandlung von Union-Typen in Tupel untersucht und alternative Lösungen vorgestellt.

Das Problem der Bestimmung des resultierenden Typs

Betrachten wir den folgenden Union-Typ:

type Union = "string" | 0 | true;

Bei dem Versuch, diesen Union-Typ in ein Tupel zu konvertieren, stellt sich die Frage, wie das resultierende Tupel aussehen sollte. Es gibt mehrere mögliche Optionen:

Option 1: Alle möglichen Permutationen

type TupleOption1 =
  | ["string", 0, true]
  | ["string", true, 0]
  | [0, "string", true]
  | [0, true, "string"]
  | [true, "string", 0]
  | [true, 0, "string"];

Diese Option enthält alle möglichen Permutationen der Union-Typen und stellt sicher, dass jeder mögliche Wert abgedeckt ist und genau einmal Verwendung findet.

Option 2: Eine spezifische Reihenfolge

type TupleOption2 = ["string", 0, true];

Hier wird eine bestimmte Reihenfolge festgelegt, die jedoch nicht alle möglichen Kombinationen repräsentiert.

Option 3: Ein Tupel mit Union-Typen als Elementen

type TupleOption3 = [
  "string" | 0 | true,
  "string" | 0 | true,
  "string" | 0 | true
];

In dieser Option gehen die Einzigartigkeit und die spezifische Reihenfolge der Elemente verloren.

Fazit: Bei der Umwandlung eines Union-Typs in ein Tupel ist es wichtig, einen Tupel-Typ zu wählen, der alle möglichen Werte des ursprünglichen Union-Typs repräsentieren kann. TupleOption1 ist hierbei die flexibelste Wahl, aber es kommt auf den Anwendungfall an.

Probleme bei der Umwandlung von Union-Typen in Tupel

Unvorhersehbare Reihenfolge der Elemente

Die Reihenfolge eines Unions ist zufällig, daher kann eine bestimmte Reihenfolge der Elemente im resultierenden Tupel kann nicht garantiert werden und kann sich über die Zeit ändern, insbesondere wenn dem Union-Typ neue Elemente hinzugefügt werden oder sich die interne Implementierung von TypeScript ändert.

Unions in primitiven Typen

Primitive Typen wie boolean sind selbst Unions (true | false) . Beim Versuch, solche Typen in Tupel umzuwandeln, können unerwartete Ergebnisse auftreten.

type TupleFromBooleanUnion = UnionToTuple<boolean>; 
// [true, false] oder [false, true], aber NICHT [boolean]

Ähnlich verhält es sich mit erweiterten Typen wie string | "a":

type TupleWithWidened = UnionToTuple<string | "a">; // [string], aber NICHT [string, "a"]

Kombiniert man diese Typen:

type TupleWithWidenedAndBoolean = UnionToTuple<string | "a" | boolean>; // [string, false, true] or [...], aber NICHT [string, "a", boolean]

Kompilationsprobleme und Performance

Der TypeScript-Compiler kann sein festgelegtes Rekursionslimit erreichen, wenn er versucht, alle möglichen Permutationen eines Union-Typs zu generieren. Dies führt zu Performance-Problemen, besonders bei großen Union-Typen.

Eingeschränkte Iterationsmöglichkeiten

TypeScript bietet keine native Möglichkeit, über die Elemente eines Union-Typs – in einer festgelegten Reihenfolge– zu iterieren. Workarounds beeinträchtigen oft die Kompilationsleistung erheblich.

Eine alternative Lösung

Statt zu versuchen, einen Union-Typ in ein Tupel umzuwandeln, ist es oft besser, den Ansatz zu ändern.

Problemstellung

Betrachten wir folgendes Beispiel:

type Contract = {
  name: string;
  age: number;
  isAlive: boolean;
};

type ContractKey = keyof Contract;
const contractKeys: ContractKey[] = ["name", "age", "isAlive"];

function isContractKey(key: string): key is ContractKey {
  return contractKeys.includes(key as ContractKeys);
}

Problem: Wenn dem Contract-Typ ein neuer Schlüssel hinzugefügt wird, kann man sich nicht sicher sein, dass contractKeys aktualisiert wird. Dies führt zu einem Bug in isContractKey , der von TypeScript nicht abgefangen werden kann

Lösung

Beginnen wir mit der Definition von contractKeys und leiten daraus die anderen Typen ab:

const contractKeys = ["name", "age", "isAlive"] as const;
type ContractKey = typeof contractKeys[number];
type Contract = Record<ContractKey, unknown>;

function isContractKey(key: string): key is ContractKey {
  return contractKeys.includes(key as ContractKey);
}

Durch diesen Ansatz wird sichergestellt, dass alle Schlüssel konsistent sind. Wenn ein neuer Schlüssel hinzugefügt wird, muss er nur an einer Stelle aktualisiert werden, und TypeScript wird Fehler anzeigen, wenn die Definitionen nicht übereinstimmen.

Fazit

Die Umwandlung eines Union-Typs in ein Tupel in TypeScript bringt mehrere Herausforderungen mit sich, darunter unvorhersehbare Reihenfolgen, Kompilationsprobleme und Performance-Einschränkungen. Statt diese Probleme zu umgehen, ist es oft effektiver, den Ansatz zu überdenken und Lösungen zu implementieren, die besser mit den Stärken von TypeScript harmonieren.

Durch die sorgfältige Strukturierung des Codes und die Nutzung des Typensystems von TypeScript können robuste und wartbare Anwendungen erstellt werden, ohne auf komplizierte und fehleranfällige Typumwandlungen zurückgreifen zu müssen.