لغة TypeScript

الأنواع ذات العلامات التجارية والكتابة الاسمية

35 دقيقة الدرس 26 من 40

مقدمة إلى الأنواع ذات العلامات التجارية

يستخدم TypeScript الكتابة البنيوية افتراضياً، مما يعني أن الأنواع متوافقة بناءً على بنيتها بدلاً من أسمائها. ومع ذلك، هناك حالات تحتاج فيها إلى سلوك الكتابة الاسمية - التمييز بين الأنواع بناءً على هويتها وليس فقط بنيتها. توفر الأنواع ذات العلامات التجارية هذه القدرة من خلال تقنية ذكية.

المشكلة مع الكتابة البنيوية

لنأخذ هذا السيناريو كمثال:

type UserId = number; type ProductId = number; function getUser(id: UserId): string { return `User ${id}`; } const productId: ProductId = 42; getUser(productId); // لا يوجد خطأ! لكن هذا خطأ دلالياً

كل من UserId و ProductId متطابقان بنيوياً (مجرد number)، لذا يسمح TypeScript باستخدامهما بالتبادل. هذا يمكن أن يؤدي إلى أخطاء دقيقة.

إنشاء الأنواع ذات العلامات التجارية

تضيف الأنواع ذات العلامات التجارية خاصية وهمية فريدة للتمييز بين الأنواع:

type Brand<K, T> = K & { __brand: T }; type UserId = Brand<number, 'UserId'>; type ProductId = Brand<number, 'ProductId'>; function getUser(id: UserId): string { return `User ${id}`; } // نحتاج تأكيد النوع لإنشاء قيم ذات علامات تجارية const userId = 42 as UserId; const productId = 42 as ProductId; getUser(userId); // ✓ صحيح getUser(productId); // ✗ خطأ: ProductId غير قابل للتعيين إلى UserId
ملاحظة: خاصية __brand هي نوع وهمي - موجودة فقط في وقت التجميع وليس لها تمثيل في وقت التشغيل. هذا يعني أن الأنواع ذات العلامات التجارية ليس لها أي تكلفة في وقت التشغيل.

دوال المصنع لسلامة الأنواع

بدلاً من تأكيدات الأنواع في كل مكان، أنشئ دوال مصنع:

type UserId = Brand<number, 'UserId'>; type Email = Brand<string, 'Email'>; function createUserId(id: number): UserId { if (id <= 0) { throw new Error('Invalid user ID'); } return id as UserId; } function createEmail(email: string): Email { if (!email.includes('@')) { throw new Error('Invalid email format'); } return email as Email; } // الآن لديك التحقق في وقت التشغيل + السلامة في وقت التجميع const user = createUserId(42); const email = createEmail('user@example.com');

العلامات التجارية متعددة المستويات

يمكنك إنشاء تسلسلات هرمية من الأنواع ذات العلامات التجارية:

type Id = Brand<number, 'Id'>; type UserId = Brand<Id, 'UserId'>; type AdminId = Brand<UserId, 'AdminId'>; function processId(id: Id): void { console.log(`Processing ID: ${id}`); } function processUser(id: UserId): void { console.log(`Processing user: ${id}`); } function processAdmin(id: AdminId): void { console.log(`Processing admin: ${id}`); } const adminId = 1 as AdminId; processAdmin(adminId); // ✓ صحيح processUser(adminId); // ✓ صحيح (AdminId يمتد UserId) processId(adminId); // ✓ صحيح (AdminId يمتد Id) const userId = 2 as UserId; processAdmin(userId); // ✗ خطأ: UserId غير قابل للتعيين إلى AdminId

الأنواع ذات العلامات التجارية لوحدات القياس

منع خلط الوحدات غير المتوافقة:

type Meters = Brand<number, 'Meters'>; type Kilometers = Brand<number, 'Kilometers'>; type Miles = Brand<number, 'Miles'>; const meters = (value: number) => value as Meters; const kilometers = (value: number) => value as Kilometers; const miles = (value: number) => value as Miles; function addMeters(a: Meters, b: Meters): Meters { return (a + b) as Meters; } function convertKmToMeters(km: Kilometers): Meters { return (km * 1000) as Meters; } const distance1 = meters(100); const distance2 = meters(50); const distance3 = kilometers(5); addMeters(distance1, distance2); // ✓ صحيح addMeters(distance1, convertKmToMeters(distance3)); // ✓ صحيح addMeters(distance1, distance3); // ✗ خطأ: Kilometers غير قابل للتعيين

نمط الأنواع المعتمة

إنشاء أنواع معتمة حقاً تخفي تفاصيل التنفيذ:

// قم بالإعلان عن العلامة التجارية لكن لا تصدر النوع الفعلي أبداً declare const opaqueUserId: unique symbol; export type UserId = number & { readonly [opaqueUserId]: true }; // صدّر فقط دوال المصنع والوصول export function createUserId(id: number): UserId { if (id <= 0) throw new Error('Invalid ID'); return id as UserId; } export function getUserIdValue(id: UserId): number { return id as number; } // الاستخدام في ملف آخر import { UserId, createUserId, getUserIdValue } from './userId'; const id = createUserId(42); const rawValue = getUserIdValue(id); // لا يمكن إنشاء UserId مباشرة - يجب استخدام المصنع const invalid = 42 as UserId; // لا يزال ممكناً، لكنه غير مستحسن
أفضل ممارسة: استخدم الأنواع المعتمة للأنواع الخاصة بالمجال والتي يجب أن يكون لها إنشاء ووصول محكوم. هذا يفرض التغليف المناسب على مستوى النوع.

الأنواع ذات العلامات التجارية مع التحقق

دمج العلامات التجارية مع التحقق في وقت التشغيل:

type PositiveNumber = Brand<number, 'PositiveNumber'>; type Percentage = Brand<number, 'Percentage'>; type Age = Brand<number, 'Age'>; function assertPositive(n: number): asserts n is PositiveNumber { if (n <= 0) { throw new Error('Number must be positive'); } } function assertPercentage(n: number): asserts n is Percentage { if (n < 0 || n > 100) { throw new Error('Percentage must be between 0 and 100'); } } function assertAge(n: number): asserts n is Age { if (n < 0 || n > 150) { throw new Error('Invalid age'); } } function calculateDiscount(price: PositiveNumber, discount: Percentage): PositiveNumber { const result = price * (1 - discount / 100); return result as PositiveNumber; } const price = 100; assertPositive(price); // الآن price مكتوب كـ PositiveNumber const discount = 20; assertPercentage(discount); // الآن discount مكتوب كـ Percentage const finalPrice = calculateDiscount(price, discount);

أنماط النصوص الآمنة للأنواع

استخدم الأنواع ذات العلامات التجارية لأنماط النصوص المتحقق منها:

type UUID = Brand<string, 'UUID'>; type HexColor = Brand<string, 'HexColor'>; type URL = Brand<string, 'URL'>; const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const HEX_COLOR_REGEX = /^#[0-9a-f]{6}$/i; const URL_REGEX = /^https?:\/\/.+/; function createUUID(str: string): UUID { if (!UUID_REGEX.test(str)) { throw new Error('Invalid UUID format'); } return str as UUID; } function createHexColor(str: string): HexColor { if (!HEX_COLOR_REGEX.test(str)) { throw new Error('Invalid hex color format'); } return str as HexColor; } function createURL(str: string): URL { if (!URL_REGEX.test(str)) { throw new Error('Invalid URL format'); } return str as URL; } function setBackgroundColor(color: HexColor): void { document.body.style.backgroundColor = color; } const color = createHexColor('#ff5733'); setBackgroundColor(color); // ✓ آمن للأنواع setBackgroundColor('#ff5733'); // ✗ خطأ: string غير قابل للتعيين إلى HexColor

الكتابة الاسمية مع الفئات

الفئات في TypeScript لديها بالفعل خصائص الكتابة الاسمية:

class UserId { constructor(private value: number) { if (value <= 0) throw new Error('Invalid ID'); } getValue(): number { return this.value; } } class ProductId { constructor(private value: number) { if (value <= 0) throw new Error('Invalid ID'); } getValue(): number { return this.value; } } function getUser(id: UserId): string { return `User ${id.getValue()}`; } const userId = new UserId(42); const productId = new ProductId(42); getUser(userId); // ✓ صحيح getUser(productId); // ✗ خطأ: ProductId غير قابل للتعيين إلى UserId
تحذير: للفئات تكلفة في وقت التشغيل (الذاكرة والأداء) مقارنة بالأنواع ذات العلامات التجارية، والتي هي بنيات وقت التجميع فقط. اختر بناءً على احتياجاتك.

تقنيات العلامات التجارية المتقدمة

استخدم الرموز الفريدة لضمانات أقوى:

declare const userIdBrand: unique symbol; declare const productIdBrand: unique symbol; type UserId = number & { readonly [userIdBrand]: true }; type ProductId = number & { readonly [productIdBrand]: true }; // مساعد لإنشاء أنواع ذات علامات تجارية type Branded<T, Brand extends symbol> = T & { readonly [K in Brand]: true }; declare const emailBrand: unique symbol; declare const urlBrand: unique symbol; type Email = Branded<string, typeof emailBrand>; type URL = Branded<string, typeof urlBrand>; function sendEmail(to: Email, subject: string, body: string): void { console.log(`Sending to ${to}: ${subject}`); } const email = 'user@example.com' as Email; sendEmail(email, 'Hello', 'World');

مثال من العالم الحقيقي: نظام مالي

type Currency = 'USD' | 'EUR' | 'GBP'; declare const moneyBrand: unique symbol; type Money<C extends Currency> = { readonly amount: number; readonly currency: C; readonly [moneyBrand]: true; }; function createMoney<C extends Currency>( amount: number, currency: C ): Money<C> { if (amount < 0) throw new Error('Amount cannot be negative'); return { amount, currency, [moneyBrand]: true } as Money<C>; } function addMoney<C extends Currency>( a: Money<C>, b: Money<C> ): Money<C> { return createMoney(a.amount + b.amount, a.currency); } function convertCurrency<From extends Currency, To extends Currency>( money: Money<From>, toCurrency: To, rate: number ): Money<To> { return createMoney(money.amount * rate, toCurrency); } const usd100 = createMoney(100, 'USD'); const usd50 = createMoney(50, 'USD'); const eur80 = createMoney(80, 'EUR'); addMoney(usd100, usd50); // ✓ صحيح: كلاهما USD addMoney(usd100, eur80); // ✗ خطأ: لا يمكن خلط العملات // يجب التحويل أولاً const eur80AsUsd = convertCurrency(eur80, 'USD', 1.1); addMoney(usd100, eur80AsUsd); // ✓ صحيح: كلاهما USD الآن
تمرين:
  1. أنشئ أنواعاً ذات علامات تجارية لـ Temperature مع الوحدات (سيلزيوس، فهرنهايت، كلفن)
  2. نفذ دوال التحويل بين الوحدات
  3. أنشئ نوع WeatherReport يضمن اتساق وحدات الحرارة
  4. أضف التحقق لمنع درجات الحرارة غير الصالحة (مثل أقل من الصفر المطلق)