Deutsch

Entdecken Sie TypeScript Branded Types, eine leistungsstarke Technik, um nominale Typisierung in einem strukturellen Typsystem zu erreichen. Lernen Sie, wie Sie Typsicherheit und Code-Klarheit verbessern.

TypeScript Branded Types: Nominale Typisierung in einem strukturellen System

Das strukturelle Typsystem von TypeScript bietet Flexibilität, kann aber manchmal zu unerwartetem Verhalten führen. Branded Types bieten eine Möglichkeit, nominale Typisierung zu erzwingen und so die Typsicherheit und Code-Klarheit zu verbessern. Dieser Artikel untersucht Branded Types im Detail und liefert praktische Beispiele und Best Practices für deren Implementierung.

Strukturelle vs. nominale Typisierung verstehen

Bevor wir uns mit Branded Types befassen, klären wir den Unterschied zwischen struktureller und nominaler Typisierung.

Strukturelle Typisierung (Duck Typing)

In einem strukturellen Typsystem gelten zwei Typen als kompatibel, wenn sie die gleiche Struktur haben (d. h. die gleichen Eigenschaften mit den gleichen Typen). TypeScript verwendet strukturelle Typisierung. Betrachten Sie dieses Beispiel:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Gültig in TypeScript

console.log(vector.x); // Ausgabe: 10

Obwohl Point und Vector als unterschiedliche Typen deklariert sind, erlaubt TypeScript die Zuweisung eines Point-Objekts zu einer Vector-Variablen, da sie die gleiche Struktur haben. Das kann praktisch sein, aber es kann auch zu Fehlern führen, wenn man zwischen logisch unterschiedlichen Typen unterscheiden muss, die zufällig die gleiche Form haben. Denken Sie zum Beispiel an Koordinaten für Breiten- und Längengrade, die zufällig mit Bildschirm-Pixelkoordinaten übereinstimmen könnten.

Nominale Typisierung

In einem nominalen Typsystem gelten Typen nur dann als kompatibel, wenn sie den gleichen Namen haben. Selbst wenn zwei Typen die gleiche Struktur haben, werden sie als verschieden behandelt, wenn sie unterschiedliche Namen haben. Sprachen wie Java und C# verwenden nominale Typisierung.

Die Notwendigkeit von Branded Types

Die strukturelle Typisierung von TypeScript kann problematisch sein, wenn Sie sicherstellen müssen, dass ein Wert zu einem bestimmten Typ gehört, unabhängig von seiner Struktur. Betrachten Sie zum Beispiel die Darstellung von Währungen. Sie könnten unterschiedliche Typen für USD und EUR haben, aber beide könnten als Zahlen dargestellt werden. Ohne einen Mechanismus zur Unterscheidung könnten Sie versehentlich Operationen mit der falschen Währung durchführen.

Branded Types lösen dieses Problem, indem sie es Ihnen ermöglichen, unterschiedliche Typen zu erstellen, die strukturell ähnlich sind, aber vom Typsystem als verschieden behandelt werden. Dies erhöht die Typsicherheit und verhindert Fehler, die sonst durchrutschen könnten.

Implementierung von Branded Types in TypeScript

Branded Types werden mithilfe von Intersection Types und einem eindeutigen Symbol oder String-Literal implementiert. Die Idee ist, einem Typ ein „Brand“ (eine Marke) hinzuzufügen, das ihn von anderen Typen mit derselben Struktur unterscheidet.

Verwendung von Symbolen (Empfohlen)

Die Verwendung von Symbolen für das Branding wird im Allgemeinen bevorzugt, da Symbole garantiert eindeutig sind.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Das Auskommentieren der nächsten Zeile verursacht einen Typfehler
// const invalidOperation = addUSD(usd1, eur1);

In diesem Beispiel sind USD und EUR Branded Types, die auf dem number-Typ basieren. Das unique symbol stellt sicher, dass diese Typen verschieden sind. Die Funktionen createUSD und createEUR werden verwendet, um Werte dieser Typen zu erstellen, und die Funktion addUSD akzeptiert nur USD-Werte. Der Versuch, einen EUR-Wert zu einem USD-Wert zu addieren, führt zu einem Typfehler.

Verwendung von String-Literalen

Sie können auch String-Literale für das Branding verwenden, obwohl dieser Ansatz weniger robust ist als die Verwendung von Symbolen, da String-Literale nicht garantiert eindeutig sind.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Das Auskommentieren der nächsten Zeile verursacht einen Typfehler
// const invalidOperation = addUSD(usd1, eur1);

Dieses Beispiel erzielt das gleiche Ergebnis wie das vorherige, verwendet jedoch String-Literale anstelle von Symbolen. Obwohl es einfacher ist, ist es wichtig sicherzustellen, dass die für das Branding verwendeten String-Literale in Ihrer Codebasis eindeutig sind.

Praktische Beispiele und Anwendungsfälle

Branded Types können in verschiedenen Szenarien angewendet werden, in denen Sie Typsicherheit über die strukturelle Kompatibilität hinaus erzwingen müssen.

IDs

Stellen Sie sich ein System mit verschiedenen Arten von IDs vor, wie z. B. UserID, ProductID und OrderID. All diese IDs könnten als Zahlen oder Strings dargestellt werden, aber Sie möchten eine versehentliche Vermischung verschiedener ID-Typen verhindern.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... Benutzerdaten abrufen
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... Produktdaten abrufen
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// Das Auskommentieren der nächsten Zeile verursacht einen Typfehler
// const invalidCall = getUser(productID);

Dieses Beispiel zeigt, wie Branded Types verhindern können, dass eine ProductID an eine Funktion übergeben wird, die eine UserID erwartet, was die Typsicherheit erhöht.

Domänenspezifische Werte

Branded Types können auch nützlich sein, um domänenspezifische Werte mit Einschränkungen darzustellen. Zum Beispiel könnten Sie einen Typ für Prozentsätze haben, die immer zwischen 0 und 100 liegen sollten.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // Das Auskommentieren der nächsten Zeile verursacht einen Fehler zur Laufzeit
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Dieses Beispiel zeigt, wie eine Einschränkung für den Wert eines Branded Type zur Laufzeit erzwungen werden kann. Obwohl das Typsystem nicht garantieren kann, dass ein Percentage-Wert immer zwischen 0 und 100 liegt, kann die createPercentage-Funktion diese Einschränkung zur Laufzeit durchsetzen. Sie können auch Bibliotheken wie io-ts verwenden, um die Laufzeitvalidierung von Branded Types zu erzwingen.

Datums- und Zeitdarstellungen

Die Arbeit mit Daten und Zeiten kann aufgrund verschiedener Formate und Zeitzonen schwierig sein. Branded Types können helfen, zwischen verschiedenen Datums- und Zeitdarstellungen zu unterscheiden.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Validieren, dass der Datumsstring im UTC-Format ist (z. B. ISO 8601 mit Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Validieren, dass der Datumsstring im lokalen Datumsformat ist (z. B. YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Zeitzonenkonvertierung durchführen
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

Dieses Beispiel unterscheidet zwischen UTC- und lokalen Daten und stellt sicher, dass Sie in verschiedenen Teilen Ihrer Anwendung mit der korrekten Datums- und Zeitdarstellung arbeiten. Die Laufzeitvalidierung stellt sicher, dass nur korrekt formatierte Datumsstrings diesen Typen zugewiesen werden können.

Best Practices für die Verwendung von Branded Types

Um Branded Types in TypeScript effektiv zu nutzen, beachten Sie die folgenden Best Practices:

Vorteile von Branded Types

Nachteile von Branded Types

Alternativen zu Branded Types

Obwohl Branded Types eine leistungsstarke Technik zur Erreichung nominaler Typisierung in TypeScript sind, gibt es alternative Ansätze, die Sie in Betracht ziehen könnten.

Opaque Types

Opaque Types ähneln Branded Types, bieten aber eine explizitere Möglichkeit, den zugrunde liegenden Typ zu verbergen. TypeScript hat keine integrierte Unterstützung für Opaque Types, aber Sie können sie mithilfe von Modulen und privaten Symbolen simulieren.

Klassen

Die Verwendung von Klassen kann einen objektorientierteren Ansatz zur Definition unterschiedlicher Typen bieten. Obwohl Klassen in TypeScript strukturell typisiert sind, bieten sie eine klarere Trennung der Belange und können verwendet werden, um Einschränkungen durch Methoden durchzusetzen.

Bibliotheken wie `io-ts` oder `zod`

Diese Bibliotheken bieten eine ausgefeilte Laufzeit-Typvalidierung und können mit Branded Types kombiniert werden, um sowohl die Sicherheit zur Kompilierzeit als auch zur Laufzeit zu gewährleisten.

Fazit

TypeScript Branded Types sind ein wertvolles Werkzeug zur Verbesserung der Typsicherheit und Code-Klarheit in einem strukturellen Typsystem. Indem Sie einem Typ ein „Brand“ hinzufügen, können Sie nominale Typisierung erzwingen und die versehentliche Vermischung von strukturell ähnlichen, aber logisch unterschiedlichen Typen verhindern. Obwohl Branded Types eine gewisse Komplexität und einen gewissen Overhead mit sich bringen, überwiegen die Vorteile der verbesserten Typsicherheit und Wartbarkeit des Codes oft die Nachteile. Erwägen Sie die Verwendung von Branded Types in Szenarien, in denen Sie sicherstellen müssen, dass ein Wert zu einem bestimmten Typ gehört, unabhängig von seiner Struktur.

Durch das Verständnis der Prinzipien hinter struktureller und nominaler Typisierung und die Anwendung der in diesem Artikel beschriebenen Best Practices können Sie Branded Types effektiv nutzen, um robusteren und wartbareren TypeScript-Code zu schreiben. Von der Darstellung von Währungen und IDs bis hin zur Durchsetzung domänenspezifischer Einschränkungen bieten Branded Types einen flexiblen und leistungsstarken Mechanismus zur Verbesserung der Typsicherheit in Ihren Projekten.

Während Sie mit TypeScript arbeiten, erkunden Sie die verschiedenen verfügbaren Techniken und Bibliotheken zur Typvalidierung und -durchsetzung. Erwägen Sie die Verwendung von Branded Types in Verbindung mit Laufzeitvalidierungsbibliotheken wie io-ts oder zod, um einen umfassenden Ansatz zur Typsicherheit zu erreichen.

TypeScript Branded Types: Nominale Typisierung in einem strukturellen System | MLOG