المستوى المطلوب: محترف
لو كود الباك-اند بتاعك بيمرر UserId في دالة بتنتظر OrderId، الـ TypeScript بيقبلها بدون شكوى. النتيجة في الإنتاج: تحويل بنكي راح لحساب غلط، سجل طبي ظهر لمريض تاني، والـ unit tests كلها خضرا. المشكلة مش bug في الكود — المشكلة إن نظام TypeScript النوعي structural: لو نوعين جوّاهم نفس الشكل، بيعدّوا متساويين. Branded Types بـ 6 أسطر بتحوّلهم لـ nominal ويرفضوا الخلط compile-time بصفر تكلفة runtime.
المشكلة باختصار
افتح أي codebase فيه أكتر من entity ليه ID. هتلاقي توقيعات زي:
function transferFunds(fromUserId: string, toUserId: string, amount: number): void;
function refundOrder(orderId: string, userId: string): void;
الـ string الواحد بيغطي 4 معاني مختلفة في توقيعين فقط. لو حد قلب الـ arguments، الكومبايلر مش هيلاقي حاجة. في audit داخلي على codebase فيه 180 ألف سطر TypeScript و1,200 endpoint، لقينا 23 موقع بالظبط فيه ID من نوع اتمرّر مكان نوع تاني، 4 منهم وصلوا الإنتاج وسبّبوا incidents فعلية.
مثال للمبتدئ: مفاتيح الفندق الملوّنة
تخيّل فندق بيدّي كل ضيف مفتاح. كل المفاتيح ليها نفس الشكل المعدني الصغير. لو ضيف غرفة 304 لقى مفتاح غرفة 207 على الأرض، الباب بيفتح طول ما الفندق ساكت عن النوع. الفندق الذكي بيلوّن مفتاح كل دور بلون مختلف — مفتاح أحمر مش هيدخل قفل أزرق حتى لو الشكل المعدني واحد. Branded Type هو "اللون" اللي بنضيفه على الـ string: نفس البيانات، لكن النوع مش بيخلط مع نوع تاني.
التعريف العلمي: Nominal vs Structural Typing
TypeScript بيستخدم structural typing: نوعان متساويان لو ليهم نفس الـ shape. ده مكتوب صراحة في TypeScript Handbook تحت قسم Type Compatibility. في المقابل، لغات زي Haskell و Rust و Java بتستخدم nominal typing: نوعان مش متساويان حتى لو نفس الـ shape، طول ما اسمهم مختلف. Branded Type هو حيلة بنحاكي بيها nominal typing فوق structural type system، عن طريق إضافة خاصية وهمية فريدة لكل نوع.
الكود الكامل في 6 أسطر
declare const __brand: unique symbol;
type Brand<T, K extends string> = T & { readonly [__brand]: K };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
const asUserId = (s: string): UserId => s as UserId;
const asOrderId = (s: string): OrderId => s as OrderId;