العربية

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

أنواع TypeScript الحرفية: إتقان قيود القيم الدقيقة

تقدم TypeScript، وهي مجموعة شاملة من JavaScript، الكتابة الثابتة (static typing) إلى عالم تطوير الويب الديناميكي. إحدى أقوى ميزاتها هي مفهوم الأنواع الحرفية (literal types). تسمح لك الأنواع الحرفية بتحديد القيمة الدقيقة التي يمكن أن يحملها متغير أو خاصية، مما يوفر أمانًا محسنًا للأنواع ويمنع الأخطاء غير المتوقعة. ستستكشف هذه المقالة الأنواع الحرفية بعمق، وتغطي صيغتها واستخدامها وفوائدها مع أمثلة عملية.

ما هي الأنواع الحرفية؟

على عكس الأنواع التقليدية مثل string، number، أو boolean، فإن الأنواع الحرفية لا تمثل فئة واسعة من القيم. بدلاً من ذلك، فإنها تمثل قيمًا محددة وثابتة. تدعم TypeScript ثلاثة أنواع من الأنواع الحرفية:

باستخدام الأنواع الحرفية، يمكنك إنشاء تعريفات أنواع أكثر دقة تعكس القيود الفعلية لبياناتك، مما يؤدي إلى كود أكثر قوة وقابلية للصيانة.

الأنواع الحرفية النصية

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

الصيغة الأساسية

صيغة تعريف النوع الحرفي النصي مباشرة:


type AllowedValues = "value1" | "value2" | "value3";

هذا يعرّف نوعًا يسمى AllowedValues يمكن أن يحمل فقط النصوص "value1" أو "value2" أو "value3".

أمثلة عملية

1. تعريف لوحة ألوان:

تخيل أنك تبني مكتبة واجهة مستخدم وتريد التأكد من أن المستخدمين يمكنهم فقط تحديد الألوان من لوحة محددة مسبقًا:


type Color = "red" | "green" | "blue" | "yellow";

function paintElement(element: HTMLElement, color: Color) {
  element.style.backgroundColor = color;
}

paintElement(document.getElementById("myElement")!, "red"); // صحيح
paintElement(document.getElementById("myElement")!, "purple"); // خطأ: Argument of type '"purple"' is not assignable to parameter of type 'Color'.

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

2. تعريف نقاط نهاية API:

عند العمل مع واجهات برمجة التطبيقات (APIs)، غالبًا ما تحتاج إلى تحديد نقاط النهاية المسموح بها. يمكن أن تساعد الأنواع الحرفية النصية في فرض ذلك:


type APIEndpoint = "/users" | "/posts" | "/comments";

function fetchData(endpoint: APIEndpoint) {
  // ... تنفيذ لجلب البيانات من نقطة النهاية المحددة
  console.log(`Fetching data from ${endpoint}`);
}

fetchData("/users"); // صحيح
fetchData("/products"); // خطأ: Argument of type '"/products"' is not assignable to parameter of type 'APIEndpoint'.

يضمن هذا المثال أن الدالة fetchData يمكن استدعاؤها فقط بنقاط نهاية API صالحة، مما يقلل من خطر الأخطاء الناتجة عن الأخطاء الإملائية أو أسماء نقاط النهاية غير الصحيحة.

3. التعامل مع لغات مختلفة (التدويل - i18n):

في التطبيقات العالمية، قد تحتاج إلى التعامل مع لغات مختلفة. يمكنك استخدام الأنواع الحرفية النصية لضمان أن تطبيقك يدعم فقط اللغات المحددة:


type Language = "en" | "es" | "fr" | "de" | "zh";

function translate(text: string, language: Language): string {
  // ... تنفيذ لترجمة النص إلى اللغة المحددة
  console.log(`Translating '${text}' to ${language}`);
  return "Translated text"; // قيمة مؤقتة
}

translate("Hello", "en"); // صحيح
translate("Hello", "ja"); // خطأ: Argument of type '"ja"' is not assignable to parameter of type 'Language'.

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

الأنواع الحرفية الرقمية

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

الصيغة الأساسية

صيغة تعريف النوع الحرفي الرقمي تشبه الأنواع الحرفية النصية:


type StatusCode = 200 | 404 | 500;

هذا يعرّف نوعًا يسمى StatusCode يمكن أن يحمل فقط الأرقام 200 أو 404 أو 500.

أمثلة عملية

1. تعريف رموز حالة HTTP:

يمكنك استخدام الأنواع الحرفية الرقمية لتمثيل رموز حالة HTTP، مما يضمن استخدام الرموز الصالحة فقط في تطبيقك:


type HTTPStatus = 200 | 400 | 401 | 403 | 404 | 500;

function handleResponse(status: HTTPStatus) {
  switch (status) {
    case 200:
      console.log("Success!");
      break;
    case 400:
      console.log("Bad Request");
      break;
    // ... حالات أخرى
    default:
      console.log("Unknown Status");
  }
}

handleResponse(200); // صحيح
handleResponse(600); // خطأ: Argument of type '600' is not assignable to parameter of type 'HTTPStatus'.

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

2. تمثيل خيارات ثابتة:

يمكنك استخدام الأنواع الحرفية الرقمية لتمثيل خيارات ثابتة في كائن تكوين:


type RetryAttempts = 1 | 3 | 5;

interface Config {
  retryAttempts: RetryAttempts;
}

const config1: Config = { retryAttempts: 3 }; // صحيح
const config2: Config = { retryAttempts: 7 }; // خطأ: Type '{ retryAttempts: 7; }' is not assignable to type 'Config'.

يحد هذا المثال من القيم الممكنة لـ retryAttempts إلى مجموعة محددة، مما يحسن من وضوح وموثوقية التكوين الخاص بك.

الأنواع الحرفية المنطقية

تمثل الأنواع الحرفية المنطقية القيم المحددة true أو false. على الرغم من أنها قد تبدو أقل تنوعًا من الأنواع الحرفية النصية أو الرقمية، إلا أنها يمكن أن تكون مفيدة في سيناريوهات محددة.

الصيغة الأساسية

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


type IsEnabled = true | false;

ومع ذلك، فإن استخدام true | false مباشرة هو أمر زائد عن الحاجة لأنه يعادل نوع boolean. تكون الأنواع الحرفية المنطقية أكثر فائدة عند دمجها مع أنواع أخرى أو في الأنواع الشرطية.

أمثلة عملية

1. منطق شرطي مع التكوين:

يمكنك استخدام الأنواع الحرفية المنطقية للتحكم في سلوك دالة بناءً على علامة تكوين:


interface FeatureFlags {
  darkMode: boolean;
  newUserFlow: boolean;
}

function initializeApp(flags: FeatureFlags) {
  if (flags.darkMode) {
    // تمكين الوضع الداكن
    console.log("Enabling dark mode...");
  } else {
    // استخدام الوضع الفاتح
    console.log("Using light mode...");
  }

  if (flags.newUserFlow) {
    // تمكين تدفق المستخدم الجديد
    console.log("Enabling new user flow...");
  } else {
    // استخدام تدفق المستخدم القديم
    console.log("Using old user flow...");
  }
}

initializeApp({ darkMode: true, newUserFlow: false });

بينما يستخدم هذا المثال النوع boolean القياسي، يمكنك دمجه مع الأنواع الشرطية (سيتم شرحها لاحقًا) لإنشاء سلوك أكثر تعقيدًا.

2. الاتحادات المُميَّزة (Discriminated Unions):

يمكن استخدام الأنواع الحرفية المنطقية كمُميِّزات في أنواع الاتحاد. ضع في اعتبارك المثال التالي:


interface SuccessResult {
  success: true;
  data: any;
}

interface ErrorResult {
  success: false;
  error: string;
}

type Result = SuccessResult | ErrorResult;

function processResult(result: Result) {
  if (result.success) {
    console.log("Success:", result.data);
  } else {
    console.error("Error:", result.error);
  }
}

processResult({ success: true, data: { name: "John" } });
processResult({ success: false, error: "Failed to fetch data" });

هنا، تعمل خاصية success، وهي نوع حرفي منطقي، كمُميِّز، مما يسمح لـ TypeScript بتضييق نوع result داخل عبارة if.

دمج الأنواع الحرفية مع أنواع الاتحاد

تكون الأنواع الحرفية أكثر قوة عند دمجها مع أنواع الاتحاد (باستخدام عامل |). هذا يسمح لك بتعريف نوع يمكن أن يحمل إحدى القيم المحددة المتعددة.

أمثلة عملية

1. تعريف نوع للحالة (Status):


type Status = "pending" | "in progress" | "completed" | "failed";

interface Task {
  id: number;
  description: string;
  status: Status;
}

const task1: Task = { id: 1, description: "Implement login", status: "in progress" }; // صحيح
const task2: Task = { id: 2, description: "Implement logout", status: "done" };       // خطأ: Type '{ id: number; description: string; status: string; }' is not assignable to type 'Task'.

يوضح هذا المثال كيفية فرض مجموعة محددة من قيم الحالة المسموح بها لكائن Task.

2. تعريف نوع للجهاز (Device):

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


type DeviceType = "mobile" | "tablet" | "desktop";

function logDeviceType(device: DeviceType) {
  console.log(`Device type: ${device}`);
}

logDeviceType("mobile"); // صحيح
logDeviceType("smartwatch"); // خطأ: Argument of type '"smartwatch"' is not assignable to parameter of type 'DeviceType'.

يضمن هذا المثال أن الدالة logDeviceType تُستدعى فقط بأنواع الأجهزة الصالحة.

الأنواع الحرفية مع الأسماء المستعارة للأنواع (Type Aliases)

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

أمثلة عملية

1. تعريف نوع لرمز العملة:


type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";

function formatCurrency(amount: number, currency: CurrencyCode): string {
  // ... تنفيذ لتنسيق المبلغ بناءً على رمز العملة
  console.log(`Formatting ${amount} in ${currency}`);
  return "Formatted amount"; // قيمة مؤقتة
}

formatCurrency(100, "USD"); // صحيح
formatCurrency(200, "CAD"); // خطأ: Argument of type '"CAD"' is not assignable to parameter of type 'CurrencyCode'.

يعرّف هذا المثال اسمًا مستعارًا لنوع CurrencyCode لمجموعة من رموز العملات، مما يحسن من قابلية قراءة الدالة formatCurrency.

2. تعريف نوع ليوم من أيام الأسبوع:


type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

function isWeekend(day: DayOfWeek): boolean {
  return day === "Saturday" || day === "Sunday";
}

console.log(isWeekend("Monday"));   // false
console.log(isWeekend("Saturday")); // true
console.log(isWeekend("Funday"));   // خطأ: Argument of type '"Funday"' is not assignable to parameter of type 'DayOfWeek'.

الاستدلال الحرفي (Literal Inference)

يمكن لـ TypeScript في كثير من الأحيان استنتاج الأنواع الحرفية تلقائيًا بناءً على القيم التي تقوم بتعيينها للمتغيرات. هذا مفيد بشكل خاص عند العمل مع متغيرات const.

أمثلة عملية

1. استدلال الأنواع الحرفية النصية:


const apiKey = "your-api-key"; // يستنتج TypeScript أن نوع apiKey هو "your-api-key"

function validateApiKey(key: "your-api-key") {
  return key === "your-api-key";
}

console.log(validateApiKey(apiKey)); // true

const anotherKey = "invalid-key";
console.log(validateApiKey(anotherKey)); // خطأ: Argument of type 'string' is not assignable to parameter of type '"your-api-key"'.

في هذا المثال، يستنتج TypeScript نوع apiKey كنوع حرفي نصي "your-api-key". ومع ذلك، إذا قمت بتعيين قيمة غير ثابتة لمتغير، فسيستنتج TypeScript عادةً النوع الأوسع string.

2. استدلال الأنواع الحرفية الرقمية:


const port = 8080; // يستنتج TypeScript أن نوع port هو 8080

function startServer(portNumber: 8080) {
  console.log(`Starting server on port ${portNumber}`);
}

startServer(port); // صحيح

const anotherPort = 3000;
startServer(anotherPort); // خطأ: Argument of type 'number' is not assignable to parameter of type '8080'.

استخدام الأنواع الحرفية مع الأنواع الشرطية

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

الصيغة الأساسية

صيغة النوع الشرطي هي:


TypeA extends TypeB ? TypeC : TypeD

هذا يعني: إذا كان TypeA قابلاً للتعيين إلى TypeB، فإن النوع الناتج هو TypeC؛ وإلا، فإن النوع الناتج هو TypeD.

أمثلة عملية

1. ربط الحالة بالرسالة:


type Status = "pending" | "in progress" | "completed" | "failed";

type StatusMessage = T extends "pending"
  ? "Waiting for action"
  : T extends "in progress"
  ? "Currently processing"
  : T extends "completed"
  ? "Task finished successfully"
  : "An error occurred";

function getStatusMessage(status: T): StatusMessage {
  switch (status) {
    case "pending":
      return "Waiting for action" as StatusMessage;
    case "in progress":
      return "Currently processing" as StatusMessage;
    case "completed":
      return "Task finished successfully" as StatusMessage;
    case "failed":
      return "An error occurred" as StatusMessage;
    default:
      throw new Error("Invalid status");
  }
}

console.log(getStatusMessage("pending"));    // Waiting for action
console.log(getStatusMessage("in progress")); // Currently processing
console.log(getStatusMessage("completed"));   // Task finished successfully
console.log(getStatusMessage("failed"));      // An error occurred

يعرّف هذا المثال نوع StatusMessage الذي يربط كل حالة ممكنة برسالة مقابلة باستخدام الأنواع الشرطية. تستفيد الدالة getStatusMessage من هذا النوع لتوفير رسائل حالة آمنة من حيث النوع.

2. إنشاء معالج أحداث آمن من حيث النوع:


type EventType = "click" | "mouseover" | "keydown";

type EventData = T extends "click"
  ? { x: number; y: number; } // بيانات حدث النقر
  : T extends "mouseover"
  ? { target: HTMLElement; }   // بيانات حدث مرور الماوس
  : { key: string; }             // بيانات حدث الضغط على المفتاح

function handleEvent(type: T, data: EventData) {
  console.log(`Handling event type ${type} with data:`, data);
}

handleEvent("click", { x: 10, y: 20 }); // صحيح
handleEvent("mouseover", { target: document.getElementById("myElement")! }); // صحيح
handleEvent("keydown", { key: "Enter" }); // صحيح

handleEvent("click", { key: "Enter" }); // خطأ: Argument of type '{ key: string; }' is not assignable to parameter of type '{ x: number; y: number; }'.

ينشئ هذا المثال نوع EventData الذي يعرّف هياكل بيانات مختلفة بناءً على نوع الحدث. هذا يسمح لك بضمان تمرير البيانات الصحيحة إلى الدالة handleEvent لكل نوع حدث.

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

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

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

الخاتمة

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