العربية

استكشف أنواع TypeScript الموسومة، وهي تقنية قوية لتحقيق الكتابة الاسمية في نظام الأنواع الهيكلي. تعلم كيفية تعزيز أمان الأنواع ووضوح الكود.

أنواع TypeScript الموسومة: الكتابة الاسمية في نظام هيكلي

يقدم نظام الأنواع الهيكلي في TypeScript مرونة ولكنه قد يؤدي أحيانًا إلى سلوك غير متوقع. توفر الأنواع الموسومة طريقة لفرض الكتابة الاسمية، مما يعزز أمان الأنواع ووضوح الكود. يستكشف هذا المقال الأنواع الموسومة بالتفصيل، ويقدم أمثلة عملية وأفضل الممارسات لتطبيقها.

فهم الكتابة الهيكلية مقابل الكتابة الاسمية

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

الكتابة الهيكلية (Duck Typing)

في نظام الأنواع الهيكلي، يعتبر نوعان متوافقين إذا كان لهما نفس الهيكل (أي نفس الخصائص بنفس الأنواع). تستخدم TypeScript الكتابة الهيكلية. انظر إلى هذا المثال:


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

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

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // صالح في TypeScript

console.log(vector.x); // المخرجات: 10

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

الكتابة الاسمية

في نظام الأنواع الاسمي، تعتبر الأنواع متوافقة فقط إذا كان لها نفس الاسم. حتى لو كان لنوعين نفس الهيكل، فإنهما يُعاملان على أنهما منفصلان إذا كان لهما اسمان مختلفان. تستخدم لغات مثل Java و C# الكتابة الاسمية.

الحاجة إلى الأنواع الموسومة

يمكن أن تكون الكتابة الهيكلية في TypeScript إشكالية عندما تحتاج إلى التأكد من أن قيمة ما تنتمي إلى نوع معين، بغض النظر عن هيكلها. على سبيل المثال، فكر في تمثيل العملات. قد يكون لديك أنواع مختلفة للدولار الأمريكي واليورو، ولكن كلاهما يمكن تمثيله كأرقام. بدون آلية للتمييز بينهما، قد تقوم عن طريق الخطأ بإجراء عمليات على العملة الخاطئة.

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

تطبيق الأنواع الموسومة في TypeScript

يتم تطبيق الأنواع الموسومة باستخدام أنواع التقاطع (intersection types) ورمز فريد أو سلسلة نصية حرفية. الفكرة هي إضافة "علامة" أو "وسم" (brand) إلى نوع لتمييزه عن الأنواع الأخرى التي لها نفس الهيكل.

استخدام الرموز (Symbols) (موصى به)

يُفضل عمومًا استخدام الرموز للوسم لأن الرموز مضمونة بأن تكون فريدة.


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

// إلغاء التعليق على السطر التالي سيسبب خطأ في النوع
// const invalidOperation = addUSD(usd1, eur1);

في هذا المثال، USD و EUR هما نوعان موسومان يعتمدان على النوع number. يضمن unique symbol أن هذه الأنواع مميزة. تُستخدم دالتا createUSD و createEUR لإنشاء قيم من هذه الأنواع، وتقبل دالة addUSD قيم USD فقط. ستؤدي محاولة إضافة قيمة EUR إلى قيمة USD إلى خطأ في النوع.

استخدام السلاسل النصية الحرفية

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


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

// إلغاء التعليق على السطر التالي سيسبب خطأ في النوع
// const invalidOperation = addUSD(usd1, eur1);

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

أمثلة عملية وحالات استخدام

يمكن تطبيق الأنواع الموسومة على سيناريوهات مختلفة حيث تحتاج إلى فرض أمان الأنواع بما يتجاوز التوافق الهيكلي.

المُعرّفات (IDs)

لننظر في نظام به أنواع مختلفة من المُعرّفات، مثل UserID و ProductID و OrderID. قد يتم تمثيل كل هذه المعرفات كأرقام أو سلاسل نصية، ولكنك تريد منع الخلط العرضي بين أنواع المعرفات المختلفة.


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 } {
  // ... جلب بيانات المستخدم
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... جلب بيانات المنتج
  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);

// إلغاء التعليق على السطر التالي سيسبب خطأ في النوع
// const invalidCall = getUser(productID);

يوضح هذا المثال كيف يمكن للأنواع الموسومة منع تمرير ProductID إلى دالة تتوقع UserID، مما يعزز أمان الأنواع.

القيم الخاصة بالمجال

يمكن أن تكون الأنواع الموسومة مفيدة أيضًا لتمثيل القيم الخاصة بالمجال مع قيود. على سبيل المثال، قد يكون لديك نوع للنسب المئوية التي يجب أن تكون دائمًا بين 0 و 100.


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

  // إلغاء التعليق على السطر التالي سيسبب خطأ أثناء التشغيل
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

يوضح هذا المثال كيفية فرض قيد على قيمة نوع موسوم أثناء وقت التشغيل. في حين أن نظام الأنواع لا يمكنه ضمان أن قيمة Percentage تكون دائمًا بين 0 و 100، يمكن لدالة createPercentage فرض هذا القيد أثناء وقت التشغيل. يمكنك أيضًا استخدام مكتبات مثل io-ts لفرض التحقق من صحة الأنواع الموسومة في وقت التشغيل.

تمثيلات التاريخ والوقت

قد يكون العمل مع التواريخ والأوقات صعبًا بسبب التنسيقات المختلفة والمناطق الزمنية. يمكن أن تساعد الأنواع الموسومة في التمييز بين تمثيلات التاريخ والوقت المختلفة.


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 {
  // التحقق من أن السلسلة النصية للتاريخ بتنسيق UTC (مثل ISO 8601 مع 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 {
  // التحقق من أن السلسلة النصية للتاريخ بتنسيق محلي (مثل 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 {
  // تنفيذ تحويل المنطقة الزمنية
  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);
}

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

أفضل الممارسات لاستخدام الأنواع الموسومة

لاستخدام الأنواع الموسومة بفعالية في TypeScript، ضع في اعتبارك أفضل الممارسات التالية:

مزايا الأنواع الموسومة

عيوب الأنواع الموسومة

بدائل الأنواع الموسومة

في حين أن الأنواع الموسومة هي تقنية قوية لتحقيق الكتابة الاسمية في TypeScript، هناك طرق بديلة قد ترغب في أخذها في الاعتبار.

الأنواع المعتمة (Opaque Types)

تشبه الأنواع المعتمة الأنواع الموسومة ولكنها توفر طريقة أكثر صراحة لإخفاء النوع الأساسي. لا تدعم TypeScript الأنواع المعتمة بشكل مدمج، ولكن يمكنك محاكاتها باستخدام الوحدات (modules) والرموز الخاصة.

الفئات (Classes)

يمكن أن يوفر استخدام الفئات نهجًا أكثر توجهاً للكائنات لتعريف أنواع مميزة. بينما تخضع الفئات للكتابة الهيكلية في TypeScript، فإنها توفر فصلاً أوضح للمسؤوليات ويمكن استخدامها لفرض القيود من خلال التوابع (methods).

مكتبات مثل `io-ts` أو `zod`

توفر هذه المكتبات تحققًا متطورًا من الأنواع في وقت التشغيل ويمكن دمجها مع الأنواع الموسومة لضمان الأمان في وقت الترجمة ووقت التشغيل.

الخاتمة

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

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

أثناء عملك مع TypeScript، استكشف التقنيات والمكتبات المختلفة المتاحة للتحقق من الأنواع وفرضها. ضع في اعتبارك استخدام الأنواع الموسومة جنبًا إلى جنب مع مكتبات التحقق في وقت التشغيل مثل io-ts أو zod لتحقيق نهج شامل لأمان الأنواع.