لغة TypeScript

المصفوفات والصفوف في TypeScript

20 دقيقة الدرس 4 من 40

العمل مع المصفوفات في TypeScript

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

المصفوفات المكتوبة (Typed Arrays)

بناء الجملة الأساسي للمصفوفات

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

// الطريقة 1: النوع متبوعاً بـ []
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let flags: boolean[] = [true, false, true];

// الطريقة 2: نوع Array العام
let numbers: Array<number> = [1, 2, 3, 4, 5];
let names: Array<string> = ["Alice", "Bob", "Charlie"];
let flags: Array<boolean> = [true, false, true];

النمط المفضل: يفضل معظم مطوري TypeScript بناء الجملة type[] على Array<type> لأنه أكثر إيجازاً وأسهل في القراءة. ومع ذلك، كلاهما مكافئ وظيفياً.

استنتاج النوع مع المصفوفات

يمكن لـ TypeScript استنتاج أنواع المصفوفات من قيمها الأولية:

// يستنتج TypeScript هذه كمصفوفات مكتوبة
let numbers = [1, 2, 3];           // يُستنتج كـ number[]
let names = ["Alice", "Bob"];      // يُستنتج كـ string[]
let mixed = [1, "two", true];      // يُستنتج كـ (string | number | boolean)[]

// المصفوفات الفارغة تُعتبر افتراضياً any[]
let empty = [];                    // يُستنتج كـ any[]
empty.push(1);                     // OK
empty.push("hello");               // OK - ليس مثالياً!

// أفضل: اكتب المصفوفات الفارغة صراحةً
let numbers: number[] = [];        // مكتوب كـ number[]
numbers.push(1);                   // OK
numbers.push("hello");             // Error!

عمليات المصفوفة مع سلامة النوع

يضمن TypeScript سلامة النوع لجميع عمليات المصفوفة:

let numbers: number[] = [1, 2, 3];

// إضافة عناصر
numbers.push(4);           // OK
numbers.push("5");         // Error: Argument of type 'string' is not assignable to parameter of type 'number'

// الوصول إلى العناصر
let first = numbers[0];    // Type: number
let last = numbers[numbers.length - 1];  // Type: number

// أساليب المصفوفة تحافظ على سلامة النوع
let doubled = numbers.map(n => n * 2);        // Type: number[]
let strings = numbers.map(n => n.toString()); // Type: string[]
let evens = numbers.filter(n => n % 2 === 0); // Type: number[]

// التقليص
let sum = numbers.reduce((acc, n) => acc + n, 0);  // Type: number

المصفوفات متعددة الأبعاد

يمكنك إنشاء مصفوفات من المصفوفات للبيانات متعددة الأبعاد:

// مصفوفة ثنائية الأبعاد (مصفوفة)
let matrix: number[][] = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

// الوصول إلى العناصر
let element = matrix[0][1];  // 2

// مصفوفة ثلاثية الأبعاد
let cube: number[][][] = [
    [[1, 2], [3, 4]],
    [[5, 6], [7, 8]]
];

// مصفوفة من الكائنات
interface User {
    name: string;
    age: number;
}

let users: User[] = [
    { name: "Alice", age: 30 },
    { name: "Bob", age: 25 }
];

المصفوفات للقراءة فقط (Readonly Arrays)

يوفر TypeScript مصفوفات للقراءة فقط لا يمكن تعديلها بعد الإنشاء:

// مصفوفة للقراءة فقط باستخدام نوع ReadonlyArray
let numbers: ReadonlyArray<number> = [1, 2, 3];

numbers[0] = 10;           // Error: Index signature in type 'readonly number[]' only permits reading
numbers.push(4);           // Error: Property 'push' does not exist on type 'readonly number[]'
numbers.pop();             // Error: Property 'pop' does not exist on type 'readonly number[]'

// مصفوفة للقراءة فقط باستخدام معدِّل readonly (مفضل)
let names: readonly string[] = ["Alice", "Bob"];

names[0] = "Charlie";      // Error
names.push("David");       // Error

// لا يزال بإمكانك القراءة من المصفوفات للقراءة فقط
console.log(numbers[0]);   // OK
console.log(numbers.length);  // OK
let copy = [...numbers];   // OK - ينشئ نسخة قابلة للتغيير

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

معلمات الدالة مع المصفوفات للقراءة فقط

// دالة لا تعدل المصفوفة
function sum(numbers: readonly number[]): number {
    // numbers.push(0);  // Error - لا يمكن تعديل المصفوفة للقراءة فقط
    return numbers.reduce((acc, n) => acc + n, 0);
}

// يمكن استدعاؤها بكل من المصفوفات القابلة للتغيير وللقراءة فقط
let mutable = [1, 2, 3];
let immutable: readonly number[] = [4, 5, 6];

console.log(sum(mutable));    // OK
console.log(sum(immutable));  // OK

فهم الصفوف (Tuples)

الصفوف هي مصفوفات بطول ثابت وأنواع محددة لكل موضع. إنها مثالية لتمثيل البيانات المنظمة حيث كل موضع له معنى محدد.

بناء الجملة الأساسي للصفوف

// صف بعنصرين
let person: [string, number] = ["Alice", 30];

// الوصول إلى عناصر الصف
let name = person[0];     // Type: string
let age = person[1];      // Type: number

// خطأ: أنواع خاطئة
let invalid: [string, number] = [30, "Alice"];  // Error: Type 'number' is not assignable to type 'string'

// خطأ: طول خاطئ
let tooShort: [string, number] = ["Alice"];     // Error: Type '[string]' is not assignable to type '[string, number]'
let tooLong: [string, number] = ["Alice", 30, "extra"];  // Error

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

// إحداثيات
type Point = [number, number];
let origin: Point = [0, 0];
let point: Point = [10, 20];

// لون RGB
type RGB = [number, number, number];
let red: RGB = [255, 0, 0];
let blue: RGB = [0, 0, 255];

// استجابة HTTP
type Response = [number, string];
let success: Response = [200, "OK"];
let notFound: Response = [404, "Not Found"];

// سجل قاعدة بيانات
type User = [number, string, string, boolean];  // [id, name, email, isActive]
let user: User = [1, "Alice", "alice@example.com", true];

عناصر الصف الاختيارية

يمكن أن تكون عناصر الصف اختيارية باستخدام معدِّل ?:

// صف مع عنصر اختياري
type User = [string, number, string?];  // name, age, email (اختياري)

let user1: User = ["Alice", 30, "alice@example.com"];  // OK
let user2: User = ["Bob", 25];                         // OK - البريد الإلكتروني اختياري
let user3: User = ["Charlie"];                         // Error - العمر مطلوب

// يجب أن تأتي العناصر الاختيارية بعد المطلوبة
type Invalid = [string?, number];  // Error: A required element cannot follow an optional element

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

يمكن أن تحتوي الصفوف على عناصر راحة لأقسام متغيرة الطول:

// صف مع عنصر راحة
type StringNumberBooleans = [string, number, ...boolean[]];

let valid1: StringNumberBooleans = ["hello", 42];
let valid2: StringNumberBooleans = ["hello", 42, true];
let valid3: StringNumberBooleans = ["hello", 42, true, false, true];

// يمكن أن يكون عنصر الراحة في المنتصف
type Numbers = [number, ...number[], number];
let nums: Numbers = [1, 2, 3, 4, 5];  // الأول والأخير يجب أن يكونا أرقاماً

// أنواع متعددة في الراحة
type Mixed = [string, ...(number | boolean)[]];
let mixed: Mixed = ["start", 1, true, 2, false];

تسميات الصف (Tuple Labels)

يسمح TypeScript 4.0+ بوضع تسميات لعناصر الصف لقراءة أفضل:

// بدون تسميات
type Point = [number, number];

// مع التسميات (أكثر وصفاً)
type Point = [x: number, y: number];

// مثال معقد
type User = [
    id: number,
    name: string,
    email: string,
    isActive: boolean
];

let user: User = [1, "Alice", "alice@example.com", true];

// التسميات لا تغير سلوك وقت التشغيل، فقط تحسّن القراءة
// لا تزال تصل عن طريق الفهرس
console.log(user[0]);  // 1
console.log(user[1]);  // "Alice"

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

الصفوف للقراءة فقط

تماماً مثل المصفوفات، يمكن أن تكون الصفوف للقراءة فقط:

// صف للقراءة فقط
let point: readonly [number, number] = [10, 20];

point[0] = 5;  // Error: Cannot assign to '0' because it is a read-only property

// للقراءة فقط مع التسميات
type Point = readonly [x: number, y: number];
let origin: Point = [0, 0];
// origin[0] = 1;  // Error

// دالة تقبل صفاً للقراءة فقط
function distance(point: readonly [number, number]): number {
    return Math.sqrt(point[0] ** 2 + point[1] ** 2);
}

تفكيك المصفوفات والصفوف

يحافظ TypeScript على سلامة النوع أثناء التفكيك:

// تفكيك المصفوفة
let numbers = [1, 2, 3, 4, 5];
let [first, second, ...rest] = numbers;
// first: number, second: number, rest: number[]

// تفكيك الصف
type Point = [number, number];
let point: Point = [10, 20];
let [x, y] = point;
// x: number, y: number

// تفكيك الصف المسمى
type User = [id: number, name: string, email: string];
let user: User = [1, "Alice", "alice@example.com"];
let [id, name, email] = user;
// id: number, name: string, email: string

// تخطي العناصر
let [, , third] = numbers;  // third: number

// القيم الافتراضية
let [a, b, c = 0] = [1, 2];
// a: number, b: number, c: number (افتراضياً 0)

مشغل الانتشار مع المصفوفات والصفوف

// نشر المصفوفات
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let combined = [...arr1, ...arr2];  // Type: number[]

// نشر الصفوف
type Point2D = [number, number];
type Point3D = [number, number, number];

let point2D: Point2D = [10, 20];
let point3D: Point3D = [...point2D, 30];  // OK

// خلط الأنواع
let mixed = [...arr1, "hello", ...arr2];  // Type: (string | number)[]

// الانتشار في استدعاءات الدوال
function sum(a: number, b: number, c: number): number {
    return a + b + c;
}

let numbers: [number, number, number] = [1, 2, 3];
sum(...numbers);  // OK - انتشار الصف يطابق عدد المعلمات

متى نستخدم المصفوفات مقابل الصفوف

استخدم المصفوفات عندما:

  • جميع العناصر لها نفس النوع
  • الطول متغير أو غير معروف
  • تحتاج إلى أساليب المصفوفة مثل map وfilter وreduce
  • تعمل مع مجموعات من العناصر المتشابهة

استخدم الصفوف عندما:

  • لديك عدد ثابت من العناصر
  • كل موضع له معنى محدد ونوع مختلف ربما
  • تمثل بيانات منظمة (مثل الإحداثيات، سجلات قاعدة البيانات)
  • تحتاج إلى إرجاع قيم متعددة من دالة

أمثلة عملية

مثال 1: دالة تُرجع قيماً متعددة

// استخدام الصف لإرجاع قيم متعددة
function getMinMax(numbers: number[]): [number, number] {
    let min = Math.min(...numbers);
    let max = Math.max(...numbers);
    return [min, max];
}

let [min, max] = getMinMax([5, 2, 8, 1, 9]);
console.log(`Min: ${min}, Max: ${max}`);

مثال 2: إدارة الحالة الآمنة من النوع

// مشابه لـ useState في React
type State<T> = [T, (newValue: T) => void];

function useState<T>(initialValue: T): State<T> {
    let value = initialValue;

    function setValue(newValue: T) {
        value = newValue;
    }

    return [value, setValue];
}

let [count, setCount] = useState(0);
setCount(5);  // OK
// setCount("5");  // Error: Argument of type 'string' is not assignable to parameter of type 'number'

مثال 3: نتيجة استعلام قاعدة البيانات

// صف قاعدة بيانات مكتوب
type UserRow = [
    id: number,
    username: string,
    email: string,
    createdAt: Date,
    isActive: boolean
];

function getUserById(id: number): UserRow | null {
    // استعلام قاعدة بيانات محاكى
    return [1, "alice", "alice@example.com", new Date(), true];
}

let user = getUserById(1);
if (user) {
    let [id, username, email, createdAt, isActive] = user;
    console.log(`${username} (${email})`);
}

تمرين عملي

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

  1. أنشئ مصفوفة مكتوبة من الأرقام واستخدم map وfilter وreduce لـ:
    • مضاعفة جميع الأرقام
    • تصفية الأرقام الأقل من 10
    • جمع الأرقام المتبقية
  2. أنشئ دالة تأخذ مصفوفة للقراءة فقط من السلاسل النصية وتُرجع أطول سلسلة نصية
  3. أنشئ نوع صف لكتاب: [title: string, author: string, year: number, isAvailable?: boolean]، ثم أنشئ مصفوفة من الكتب
  4. اكتب دالة تأخذ إحداثيات كصف [x: number, y: number] وتُرجع المسافة من الأصل
  5. أنشئ دالة تُرجع كلاً من النتيجة والخطأ لعملية كصف: [result: T | null, error: Error | null]
  6. نفذ دالة تقسم مصفوفة إلى أجزاء:
    function chunk<T>(array: T[], size: number): T[][] {
        // تنفيذك
    }
    
    console.log(chunk([1, 2, 3, 4, 5], 2));
    // Output: [[1, 2], [3, 4], [5]]

تحدي إضافي: أنشئ محلل CSV آمن من النوع يُرجع مصفوفة من الصفوف، حيث كل صف يمثل صفاً بأعمدة مكتوبة.

الملخص

  • المصفوفات في TypeScript هي مجموعات مكتوبة حيث جميع العناصر تشترك في نفس النوع
  • استخدم بناء الجملة type[] للمصفوفات (مفضل) أو Array<type>
  • المصفوفات للقراءة فقط تمنع التعديل ومفيدة للبيانات غير القابلة للتغيير
  • الصفوف هي مصفوفات بطول ثابت مع أنواع محددة لكل موضع
  • يمكن أن تكون عناصر الصف اختيارية أو تستخدم عناصر الراحة للأقسام المتغيرة
  • تسميات الصف تحسّن القراءة دون تغيير سلوك وقت التشغيل
  • التفكيك يحافظ على سلامة النوع لكل من المصفوفات والصفوف
  • استخدم المصفوفات لمجموعات من العناصر المتشابهة، والصفوف للبيانات المنظمة بمواضع ثابتة
  • يفرض TypeScript سلامة النوع لجميع عمليات المصفوفة والصف

الخطوات التالية: في الدرس التالي، سنستكشف التعدادات (enums)، ميزة TypeScript القوية لتعريف الثوابت المسماة وإنشاء كود أكثر تعبيراً.