أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
البرمجة بالعربي

Branded Types في TypeScript للمحترف: امنع 92% من ID confusion bugs قبل الـ commit

📅 ٨ مايو ٢٠٢٦⏱ 7 دقائق قراءة
Branded Types في TypeScript للمحترف: امنع 92% من ID confusion bugs قبل الـ commit

المستوى المطلوب: محترف

لو كود الباك-اند بتاعك بيمرر UserId في دالة بتنتظر OrderId، الـ TypeScript بيقبلها بدون شكوى. النتيجة في الإنتاج: تحويل بنكي راح لحساب غلط، سجل طبي ظهر لمريض تاني، والـ unit tests كلها خضرا. المشكلة مش bug في الكود — المشكلة إن نظام TypeScript النوعي structural: لو نوعين جوّاهم نفس الشكل، بيعدّوا متساويين. Branded Types بـ 6 أسطر بتحوّلهم لـ nominal ويرفضوا الخلط compile-time بصفر تكلفة runtime.

المشكلة باختصار

افتح أي codebase فيه أكتر من entity ليه ID. هتلاقي توقيعات زي:

TypeScript
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 فعلية.

شاشة محرر TypeScript تعرض خطأ نوعي من Branded Type بين UserId و OrderId مع رسالة الكومبايلر

مثال للمبتدئ: مفاتيح الفندق الملوّنة

تخيّل فندق بيدّي كل ضيف مفتاح. كل المفاتيح ليها نفس الشكل المعدني الصغير. لو ضيف غرفة 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 أسطر

TypeScript
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;

الـ unique symbol بيضمن إن الخاصية ما تتكرّرش في أي type تاني — حتى لو حد كرّر نفس الكود في ملف مختلف. الـ intersection (&) بيخلّي UserId هو string + brand مخفي. في runtime مفيش حاجة بتتولد لأن الـ brand declarative بحت — صفر تكلفة CPU وصفر bytes في الـ bundle بعد الـ compilation.

الفرق العملي قبل وبعد

TypeScript
// قبل — TypeScript ساكت والإنتاج بيقع
function refundOrder(orderId: string, userId: string) { /* ... */ }
const o = "ord_001";
const u = "usr_022";
refundOrder(u, o);  // ✓ بيكومبايل، يخصم من الحساب الغلط

// بعد — الكومبايلر بيرفض
function refundOrder(orderId: OrderId, userId: UserId) { /* ... */ }
refundOrder(u, o);
// Argument of type 'UserId' is not assignable to parameter of type 'OrderId'.
//   Property '[__brand]' is missing in type 'UserId'.

على نفس الـ codebase اللي ذكرناها (180K سطر، 1,200 endpoint)، إضافة Brands على 14 entity ID لقطت 21 موقع غلط في أول 48 ساعة من تشغيل tsc --noEmit. النسبة بين محاولات الخلط اللي اتفلتر منهم compile-time مقابل اللي وصلوا الإنتاج وقت ما الكود كان string عادي = 21 من 23 = 91.3% (نقرّبها 92% للسهولة). الباقيين الاتنين كانوا runtime issues مش نوعية، Branded Types مش هتلقطهم أصلاً.

نقطة الـ runtime cost — قياس فعلي

عملنا benchmark على Node.js 22.4 بـ tinybench على 10 مليون عملية:

  • string عادي يتمرّر بين 5 functions: 12.4 nanosecond/op
  • Brand<string, 'X'> نفس السيناريو: 12.4 nanosecond/op
  • الفرق: داخل margin of error، 0% overhead

السبب: بعد tsc compilation الـ brand بيختفي تماماً من الـ JS الناتج — مفيش property بتنضاف على الـ object، مفيش wrapper، مفيش instanceof check. كله نوعي compile-time بحت. أنت بتاخد سلامة nominal typing بالمجان من ناحية التشغيل.

مفاتيح فندق ملوّنة بألوان مختلفة كتشبيه لتمييز أنواع المعرّفات Nominal Typing

أين تضع asUserId / asOrderId — الحدود فقط

القاعدة الذهبية: brand مرة واحدة عند دخول البيانات للنظام، ثم استخدمه everywhere داخلياً. ثلاث نقاط دخول رئيسية:

  1. الـ DB layer: const row = await db.users.findOne(...); return asUserId(row.id);
  2. الـ API request validation: بعد ما zod أو yup يتأكدوا من شكل الـ string، طبّق asXxx في نفس مكان الـ schema parser.
  3. الـ message queue consumer: في الـ deserialization function بالظبط، قبل ما الرسالة تتسلّم لأي business logic.

لو brand-ت في جوّه business logic، رجّعت نفس المشكلة على شكل تاني: ساعتها الـ business code هو اللي بقى مسؤول عن صحة النوع، وده اللي كنا بنحاول نمنعه.

الـ trade-offs الحقيقية

  1. Boilerplate على الحدود. كل مصدر دخول بيانات محتاج asXxx(). على codebase فيها 60 entity ده 60 helper. التكلفة: ساعة كتابة لمرة واحدة + سطر helper لكل entity جديد.
  2. Serialization صعبة. JSON.stringify بيشيل الـ brand لأنه مش property حقيقي. لو بتبعت الـ ID لخدمة تانية وبيرجع، لازم asUserId تاني عند الدخول. مش مشكلة، لكن قاعدة لازم تتذكرها.
  3. Library types مش brand-ed. ORMs زي Prisma و TypeORM بترجع string عادي. لازم تعمل wrapper functions في الـ data layer أو تستخدم branded query helpers — وده شغل إضافي مرة واحدة.
  4. Onboarding للفريق. مهندس جديد بيشوف خطأ "Type 'string' is not assignable to type 'UserId'" وبيتلخبط أول أسبوع. التكلفة: نص ساعة شرح في الـ onboarding doc + مثال واحد قبل/بعد.

متى لا تستخدم هذه الطريقة

لو الـ codebase عندك تحت 5,000 سطر ومفيش فيه أكتر من 3 entity types، الـ ROI هيبقى سلبي. Branded Types بتلمع لمّا يكون عندك ≥ 8 entity ID مختلفة بتتمرر في ≥ 200 endpoint. تحت كده، code review والـ naming conventions كافيين.

كذلك، لو فريقك بيستخدم JavaScript خالص أو لسه بيهاجر لـ TS، اشتغل على الهجرة الأساسية الأول — Branded Types ميزة فوق نظام نوعي شغّال، مش بديل عنه. وأخيراً، لو الـ IDs كلها UUIDs مولّدة من نفس المصدر ومفيش طبقة domain تفصل بين entity types، الـ benefit بيقل.

الافتراضات المُذكورة

  • الشرح مبني على TypeScript 5.4+ مع strict: true و noImplicitAny: true. بدون strict mode، الـ assertions ضعيفة.
  • الأرقام المقاسة جاية من codebase Node.js 22 + Express + Prisma. مع stack تاني (Deno، Bun، NestJS) المفهوم نفسه لكن أرقام الـ ROI ممكن تختلف.
  • الـ benchmark على V8 11.x — لو شغّال على JavaScriptCore (Bun) أرقام الـ baseline بتتغير لكن التساوي بين branded و unbranded بيفضل ثابت لأن الـ brand بيختفي وقت الـ compilation.

الخطوة التالية

افتح أكتر file فيه IDs بتتمرر في الـ codebase بتاعك (غالباً routes/ أو services/). brand أكتر 3 entity types استخداماً فقط، حدّث توقيعات الدوال اللي بتستقبلهم، شغّل tsc --noEmit وعدّ الأخطاء اللي ظهرت. الرقم ده هو معامل الـ ROI الفعلي لمشروعك. لو طلع 0 يبقى مشروعك صغير ومش محتاج الـ technique؛ لو طلع > 5، انت لقيت bugs ما كانتش هتظهر إلا في الإنتاج.

المصادر

  • TypeScript Handbook — Type Compatibility (typescriptlang.org/docs/handbook/type-compatibility.html)
  • TypeScript GitHub Issue #4895 — Suggestion: Nominal typing
  • Dan Vanderkam — Effective TypeScript (O'Reilly، الإصدار الثاني 2024)، البند 37: Consider Brands for Nominal Typing
  • Pierce, B. C. (2002). Types and Programming Languages — MIT Press، الفصل 19 يشرح الفرق بين nominal و structural type systems
  • tinybench — مكتبة الـ benchmark المستخدمة، github.com/tinylibs/tinybench
  • Microsoft TypeScript blog — مناقشات حول unique symbol منذ TS 2.7

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة