لغة TypeScript

التعدادات في TypeScript

23 دقيقة الدرس 5 من 40

فهم التعدادات (Enums)

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

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

التعدادات الرقمية (Numeric Enums)

التعدادات الرقمية الأساسية

بشكل افتراضي، التعدادات في TypeScript رقمية. تبدأ القيمة الأولى من 0 وكل قيمة لاحقة تزداد بمقدار 1:

enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}

let playerDirection: Direction = Direction.Up;
console.log(playerDirection);  // 0

// الاستخدام في الشروط
if (playerDirection === Direction.Up) {
    console.log("Moving up!");
}

// التعدادات ثنائية الاتجاه - يمكنك الحصول على الاسم من القيمة
let directionName = Direction[0];  // "Up"
console.log(directionName);

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

يمكنك تعيين قيمة البداية صراحةً. ستزداد القيم اللاحقة من هناك:

enum HttpStatus {
    OK = 200,
    Created = 201,
    Accepted = 202,
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404,
    InternalServerError = 500
}

function handleResponse(status: HttpStatus) {
    switch (status) {
        case HttpStatus.OK:
            console.log("Success!");
            break;
        case HttpStatus.NotFound:
            console.log("Resource not found");
            break;
        case HttpStatus.InternalServerError:
            console.log("Server error");
            break;
    }
}

handleResponse(HttpStatus.OK);  // "Success!"

الزيادة التلقائية مع القيم المخصصة

يمكنك تعيين بعض القيم صراحةً والسماح للآخرين بالزيادة التلقائية:

enum Level {
    Easy = 1,     // 1
    Medium,       // 2 (زيادة تلقائية)
    Hard,         // 3 (زيادة تلقائية)
    Expert = 10,  // 10
    Master        // 11 (زيادة تلقائية)
}

console.log(Level.Easy);    // 1
console.log(Level.Medium);  // 2
console.log(Level.Expert);  // 10
console.log(Level.Master);  // 11

قيم التعداد المحسوبة

يمكن حساب قيم التعداد باستخدام التعبيرات:

enum FileAccess {
    None = 0,
    Read = 1 << 0,     // 1
    Write = 1 << 1,    // 2
    ReadWrite = Read | Write,  // 3
    Execute = 1 << 2   // 4
}

// فحص الأذونات باستخدام العمليات البتية
function hasPermission(access: FileAccess, permission: FileAccess): boolean {
    return (access & permission) === permission;
}

let myAccess = FileAccess.ReadWrite;
console.log(hasPermission(myAccess, FileAccess.Read));   // true
console.log(hasPermission(myAccess, FileAccess.Write));  // true
console.log(hasPermission(myAccess, FileAccess.Execute)); // false

التعدادات النصية (String Enums)

تسمح لك التعدادات النصية بتعيين قيم نصية لأعضاء التعداد. على عكس التعدادات الرقمية، لا تزداد التعدادات النصية تلقائياً ويجب تهيئة كل عضو صراحةً:

enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE"
}

let favoriteColor: Color = Color.Red;
console.log(favoriteColor);  // "RED"

// التعدادات النصية أكثر قابلية للقراءة في وقت التشغيل
function setTheme(color: Color) {
    console.log(`Setting theme to ${color}`);
}

setTheme(Color.Blue);  // "Setting theme to BLUE"

// التعدادات النصية ليست ثنائية الاتجاه
// Color["RED"]  // Error: Element implicitly has an 'any' type

أمثلة على التعدادات النصية من العالم الحقيقي

// نقاط نهاية API
enum ApiRoute {
    Users = "/api/users",
    Posts = "/api/posts",
    Comments = "/api/comments",
    Auth = "/api/auth"
}

async function fetchData(route: ApiRoute) {
    const response = await fetch(route);
    return response.json();
}

fetchData(ApiRoute.Users);

// أسماء الأحداث
enum EventType {
    Click = "click",
    MouseOver = "mouseover",
    KeyDown = "keydown",
    Submit = "submit"
}

document.addEventListener(EventType.Click, (e) => {
    console.log("Clicked!");
});

// أسماء جداول قاعدة البيانات
enum TableName {
    Users = "users",
    Products = "products",
    Orders = "orders",
    OrderItems = "order_items"
}

function selectFrom(table: TableName, columns: string[]) {
    return `SELECT ${columns.join(", ")} FROM ${table}`;
}

console.log(selectFrom(TableName.Users, ["id", "name", "email"]));

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

التعدادات الثابتة (Const Enums)

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

const enum Direction {
    Up,
    Down,
    Left,
    Right
}

let direction = Direction.Up;

// بعد التجميع، يصبح هذا:
// let direction = 0;  // التعداد اختفى، القيمة فقط تبقى

// الفوائد: لا كود وقت تشغيل، أداء أفضل
// العيب: لا يمكن استخدام التعيين العكسي (Direction[0] لا يعمل)

تعداد عادي مقابل تعداد ثابت

// تعداد عادي - ينتج كود وقت تشغيل
enum Status {
    Active,
    Inactive
}

// يُترجم إلى:
// var Status;
// (function (Status) {
//     Status[Status["Active"] = 0] = "Active";
//     Status[Status["Inactive"] = 1] = "Inactive";
// })(Status || (Status = {}));

// تعداد ثابت - لا كود وقت تشغيل
const enum Status {
    Active,
    Inactive
}

let status = Status.Active;
// يُترجم إلى:
// let status = 0; // القيمة فقط، لا كائن تعداد

قيود التعداد الثابت: لا يمكن استخدام التعدادات الثابتة مع القيم المحسوبة، ولا يمكنك الحصول على كائن التعداد في وقت التشغيل. استخدم التعدادات العادية إذا كنت بحاجة إلى الانعكاس أو التعيين العكسي.

التعدادات غير المتجانسة (Heterogeneous Enums)

يسمح TypeScript بخلط القيم النصية والرقمية في نفس التعداد، على الرغم من أن هذا نادراً ما يُوصى به:

enum Mixed {
    No = 0,
    Yes = "YES"
}

console.log(Mixed.No);   // 0
console.log(Mixed.Yes);  // "YES"

// التعيين العكسي يعمل فقط للقيم الرقمية
console.log(Mixed[0]);   // "No"
// Mixed["YES"]  // Error

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

أنواع أعضاء التعداد

يمكن استخدام كل عضو تعداد كنوع:

enum ShapeKind {
    Circle,
    Square
}

// استخدام عضو التعداد كنوع
interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case ShapeKind.Circle:
            return Math.PI * shape.radius ** 2;
        case ShapeKind.Square:
            return shape.sideLength ** 2;
    }
}

let circle: Circle = { kind: ShapeKind.Circle, radius: 10 };
console.log(getArea(circle));

التعيين العكسي (Reverse Mapping)

تدعم التعدادات الرقمية التعيين العكسي، مما يعني أنه يمكنك الحصول على اسم التعداد من قيمته:

enum Priority {
    Low,      // 0
    Medium,   // 1
    High      // 2
}

// التعيين الأمامي (الاسم إلى القيمة)
let value = Priority.High;  // 2

// التعيين العكسي (القيمة إلى الاسم)
let name = Priority[2];     // "High"

// التكرار على أسماء التعداد
for (let key in Priority) {
    if (isNaN(Number(key))) {
        console.log(key);  // "Low", "Medium", "High"
    }
}

// التكرار على قيم التعداد
for (let key in Priority) {
    if (!isNaN(Number(key))) {
        console.log(Priority[key]);  // "Low", "Medium", "High"
    }
}

مهم: التعيين العكسي يعمل فقط مع التعدادات الرقمية. التعدادات النصية والتعدادات الثابتة لا تدعم التعيين العكسي.

التعدادات في وقت التشغيل

فهم كيفية عمل التعدادات في وقت التشغيل يساعدك على استخدامها بفعالية:

enum Color {
    Red,
    Green,
    Blue
}

// يبدو كائن JavaScript المُولَّد مثل:
// {
//   0: "Red",
//   1: "Green",
//   2: "Blue",
//   Red: 0,
//   Green: 1,
//   Blue: 2
// }

// يمكنك فحص ما إذا كانت القيمة عضو تعداد صالح
function isValidColor(value: any): value is Color {
    return Object.values(Color).includes(value);
}

console.log(isValidColor(Color.Red));   // true
console.log(isValidColor(0));           // true
console.log(isValidColor(5));           // false

// الحصول على جميع مفاتيح التعداد
const colorKeys = Object.keys(Color).filter(k => isNaN(Number(k)));
console.log(colorKeys);  // ["Red", "Green", "Blue"]

// الحصول على جميع قيم التعداد (أرقام)
const colorValues = Object.keys(Color).filter(k => !isNaN(Number(k))).map(Number);
console.log(colorValues);  // [0, 1, 2]

التعدادات مقابل أنواع الاتحاد

في بعض الأحيان يمكنك استخدام أنواع الاتحاد بدلاً من التعدادات. إليك متى تستخدم كلاً منها:

// استخدام التعداد
enum Status {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
    Pending = "PENDING"
}

function setStatus(status: Status) {
    console.log(status);
}

// استخدام نوع الاتحاد
type Status = "ACTIVE" | "INACTIVE" | "PENDING";

function setStatus(status: Status) {
    console.log(status);
}

// كلا النهجين يعملان، لكن لهما خصائص مختلفة

مزايا التعداد:

  • يوفر مساحة اسم (Status.Active مقابل "ACTIVE")
  • الإكمال التلقائي يعمل بشكل أفضل
  • يمكن التكرار على القيم
  • أكثر توثيقاً ذاتياً

مزايا نوع الاتحاد:

  • لا كود وقت تشغيل (عند استخدام حرفيات السلسلة)
  • أكثر مرونة - يمكن دمجه مع أنواع أخرى
  • أفضل لواجهات برمجة التطبيقات المكتبية (لا تبعية تعداد)
  • أبسط لمجموعات صغيرة من القيم

أنماط التعداد العملية

النمط 1: رموز الخطأ

enum ErrorCode {
    Success = 0,
    InvalidInput = 1001,
    DatabaseError = 2001,
    NetworkError = 3001,
    Unauthorized = 4001,
    NotFound = 4004
}

class Result<T> {
    constructor(
        public code: ErrorCode,
        public data?: T,
        public message?: string
    ) {}

    isSuccess(): boolean {
        return this.code === ErrorCode.Success;
    }
}

function fetchUser(id: number): Result<User> {
    if (id < 0) {
        return new Result(ErrorCode.InvalidInput, undefined, "Invalid user ID");
    }
    // ... منطق الجلب
    return new Result(ErrorCode.Success, userData);
}

النمط 2: آلة الحالة

enum OrderState {
    Created,
    PaymentPending,
    PaymentConfirmed,
    Processing,
    Shipped,
    Delivered,
    Cancelled
}

class Order {
    private state: OrderState = OrderState.Created;

    confirmPayment() {
        if (this.state === OrderState.PaymentPending) {
            this.state = OrderState.PaymentConfirmed;
        } else {
            throw new Error("Invalid state transition");
        }
    }

    ship() {
        if (this.state === OrderState.Processing) {
            this.state = OrderState.Shipped;
        } else {
            throw new Error("Cannot ship order in current state");
        }
    }

    getState(): OrderState {
        return this.state;
    }
}

النمط 3: علامات الميزات

enum Feature {
    DarkMode = 1 << 0,        // 1
    Notifications = 1 << 1,   // 2
    AdvancedSearch = 1 << 2,  // 4
    AdminPanel = 1 << 3       // 8
}

class User {
    private features: number = 0;

    enableFeature(feature: Feature) {
        this.features |= feature;
    }

    disableFeature(feature: Feature) {
        this.features &= ~feature;
    }

    hasFeature(feature: Feature): boolean {
        return (this.features & feature) === feature;
    }
}

let user = new User();
user.enableFeature(Feature.DarkMode);
user.enableFeature(Feature.Notifications);

console.log(user.hasFeature(Feature.DarkMode));       // true
console.log(user.hasFeature(Feature.AdvancedSearch)); // false

تمرين عملي

أنشئ ملفاً يسمى enums-practice.ts وأكمل هذه المهام:

  1. أنشئ تعداد DayOfWeek بجميع الأيام السبعة. اكتب دالة تحدد ما إذا كان اليوم عطلة نهاية أسبوع.
  2. أنشئ تعداد نصي LogLevel بقيم: DEBUG، INFO، WARN، ERROR. نفذ فئة مسجل بسيطة تسجل فقط الرسائل عند أو فوق مستوى مكوّن.
  3. أنشئ تعداد Permission باستخدام علامات البت لـ: Read، Write، Delete، Admin. نفذ دوال لمنح وإلغاء والتحقق من الأذونات.
  4. أنشئ تعداد HttpMethod ودالة request عامة تقبل الأسلوب وعنوان URL:
    enum HttpMethod {
        GET = "GET",
        POST = "POST",
        PUT = "PUT",
        DELETE = "DELETE"
    }
    
    async function request(method: HttpMethod, url: string, data?: any) {
        // تنفيذك
    }
  5. أنشئ تعداد GameState (Menu، Playing، Paused، GameOver) ونفذ مدير حالة لعبة بسيط يتحقق من انتقالات الحالة.
  6. قارن حل تعداد مع حل نوع اتحاد لتعريف أنواع البطاقات. نفذ دالة خلط لكلا النهجين.

تحدي إضافي: أنشئ تعداد ثابت لأسماء ألوان CSS شائعة الاستخدام واكتب دالة تحول التعداد إلى قيم RGB دون أي كود تعداد وقت تشغيل.

الأخطاء الشائعة التي يجب تجنبها

  1. استخدام التعدادات غير المتجانسة دون داعٍ: التزم بالتعدادات الرقمية أو النصية للاتساق.
  2. نسيان أن التعدادات الرقمية ثنائية الاتجاه: تذكر أن MyEnum[0] يُرجع الاسم.
  3. استخدام التعدادات الثابتة عندما تحتاج إلى انعكاس وقت التشغيل: استخدم التعدادات العادية إذا كنت بحاجة إلى التكرار أو التعيين العكسي.
  4. عدم تهيئة أعضاء التعداد النصي: يجب أن يكون لكل عضو في تعداد نصي قيمة.
  5. مقارنة التعدادات بالسلاسل/الأرقام بشكل غير صحيح: استخدم عضو التعداد (Color.Red) بدلاً من قيمته (0).

الملخص

  • التعدادات الرقمية تبدأ من 0 بشكل افتراضي وتزداد تلقائياً؛ تدعم التعيين العكسي
  • التعدادات النصية تتطلب قيماً صريحة؛ أكثر قابلية للقراءة في وقت التشغيل؛ لا تعيين عكسي
  • التعدادات الثابتة تُدرج في وقت التجميع للأداء الأفضل؛ لا كائن وقت تشغيل
  • التعدادات غير المتجانسة تخلط السلاسل والأرقام؛ تجنب ما لم يكن ضرورياً
  • يمكن استخدام أعضاء التعداد كأنواع لفحص نوع أكثر دقة
  • التعيين العكسي يسمح بالحصول على الاسم من قيم التعداد الرقمية
  • التعدادات تولد كود وقت تشغيل (باستثناء التعدادات الثابتة)، مما يوفر مساحة اسم وقدرات تكرار
  • استخدم التعدادات لمجموعات ثابتة من القيم المرتبطة؛ فكر في أنواع الاتحاد للحالات الأبسط
  • أنماط علامات البت مع التعدادات تمكّن أنظمة أذونات/ميزات فعالة

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