Zurück
Tech 3 Min. Lesezeit - 22. März 23 - Matthieu Locussol

Type Branding & Flavoring: Machen Sie Ihren TypeScript-Code lesbarer und robuster

Motivationen

Das Typsystem von TypeScript ist strukturell und dies ist einer seiner Hauptvorteile. Diese Eigenschaft bietet viele leistungsstarke Tools, um ungültige Zustände nicht darstellbar zu machen und so potenzielle Bugs bereits bei der Kompilierung und nicht erst zur Laufzeit zu erkennen.
Dieses System ist jedoch nicht immer ausreichend. Es gibt zum Beispiel reale Anwendungsfälle, bei denen es wünschenswert ist, dass zwei Variablen unterschieden werden, weil sie einen unterschiedlichen Typnamen haben, selbst wenn sie die exakt gleiche Struktur aufweisen.
Beispiel:
type PostId = number;
type CommentId = number;

const postId: PostId = post.id;
const commentId: CommentId = postId; // OK
Es ist möglich, der Variable commentId den Inhalt der Variable postId, schliesslich sind sie beide ein number , wenn man die Definition ihres Typs betrachtet. Doch will man wirklich, dass dies möglich ist?
Aus der Sicht fachlichen unserer Anwendung, ergibt dies nicht wirklich Sinn. Ein Blogbeitrag ist nicht dasselbe wie ein Kommentar. Das trifft sich gut! Genau diese Art von fachlicher Einschränkung ermöglicht uns das Type Branding umzusetzen.

Branding

Definition

Das Konzept von Branding besteht darin, unserem Typ ein Unterscheidungsmerkmal hinzuzufügen, um ihn von anderen zu unterscheiden. Dieses Merkmal wird ausschliesslich dem TypeScript-Compiler nützlich sein, um statisch zu bestimmen, ob zwei Typen kompatibel sind oder nicht.
Hier ist unser gleiches Beispiel, welches den Branding-Typ verwendet:
type PostId = number & { __brand: 'PostId' };
type CommentId = number & { __brand: 'CommentId' };

const value = 1 as PostId;

const postId: PostId = value; // OK
const commentId: CommentId = value; // Erreur
Obwohl die Zuweisung eines Typs PostId an einen Typ CommentId zur Laufzeit kein Problem darstellt, erzeugt sie nun einen Kompilierungsfehler und vermeidet Geschäftsfehler.
Es ist üblich, einen generischen Typ zu definieren, der die Generierung von gebrandeten Typen ermöglicht:
type Brand<T, U> = T & { __brand: U };

type PostId = Brand<number, 'PostId'>;
type CommentId = Brand<number, 'CommentId'>;
Hinweis: Ein Typ wie Brand kann existieren, da die Schnittmenge zwischen number (hier) und einem JS-Objekt von JavaScript erlaubt ist. In den meisten anderen typisierten Sprachen würde eine solche Schnittmenge "never" entsprechen und wäre sinnlos.

Limitations

  • Die Umwandlung eines Typs in einen gebrandeten Typ erfordert ein manuelles Casting;
  • Es ist möglich, die Eigenschaft zu lesen __brand ;
  • Es gibt keine implizite Konvertierung, zum Beispiel:
type Post = Brand<{ author: string; content: string; }, 'Post'>;

const createPost = (post: Post) => { ... };

createPost({ author: 'matthieu', content: 'Hello world!' }); // Erreur

Flavoring

Definition

Flavoring ist in jeder Hinsicht ähnlich wie Branding, mit dem Unterschied, dass die Eigenschaft __brand optional gemacht wird. Diese Technik ermöglicht uns eine implizite Konvertierung für Typen und Objekte:
type Flavor<T, U> = T & { __flavor?: U };

type Post = Flavor<{ author: string; content: string; }, 'Post'>;
type PostComment = Flavor<{ author: string; content: string; }, 'PostComment'>;

const createPost = (post: Post) => { ... };
createPost({ author: 'matthieu', content: 'Hello world!' }); // OK

const comment: PostComment = { author: 'matthieu', content: 'Hello world!' };
createPost(comment); // Erreur
Trotzdem stellen wir fest, dass die Inkompatibilität zwischen verschiedenen Typen mit der gleichen Struktur beibehalten wurde. Das gilt auch für primitive Typen:
type Flavor<T, U> = T & { __flavor?: U };

type PostId = Flavor<number, 'PostId'>
type CommentId = Flavor<number, 'CommentId'>

const postId: PostId = 1;
const commentId: CommentId = postId; // Erreur

Limitations

  • Es ist immer noch möglich, die Eigenschaft zu lesen __flavor ;
  • Weniger strikt als Branding, kann die implizite Konvertierung zu Fehlern führen und sollte mit Bedacht eingesetzt werden

Fazit

Obwohl allgemein anerkannt ist, dass es vorzuziehen ist, Branding für primitive Typen zu verwenden, kann Flavoring für Objekte vorzuziehen sein, um von der impliziten Konvertierung zu profitieren. Wir können einen bedingten Typ verwenden, um diese Arbeit für uns zu erledigen:
type Brand<T, U> = T & { __brand: U };
type Flavor<T, U> = T & { __flavor?: U };

type Nominal<T, U> = T extends object ? Flavor<T, U> : Brand<T, U>;

Referenzen

Möchten Sie bei der Lancierung Ihres digitalen Projekts begleitet werden?

Reichen Sie Ihr Projekt jetzt ein