لغة TypeScript

الدوال في TypeScript

30 دقيقة الدرس 8 من 40

الدوال في TypeScript

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

تعليقات أنواع الدوال

الطريقة الأساسية الأكثر إضافة الأنواع إلى الدوال هي من خلال إضافة تعليقات المعاملات وأنواع الإرجاع:

أنواع الدوال الأساسية:

// دالة مع تعليقات المعامل ونوع الإرجاع
function add(a: number, b: number): number {
  return a + b;
}

// دالة السهم مع تعليقات الأنواع
const multiply = (a: number, b: number): number => {
  return a * b;
};

// دالة السهم المختصرة
const subtract = (a: number, b: number): number => a - b;

// دالة بدون قيمة إرجاع (void)
function logMessage(message: string): void {
  console.log(message);
}

// الاستخدام
const sum = add(5, 3);           // sum: number = 8
const product = multiply(4, 7);  // product: number = 28
logMessage('Hello, TypeScript!'); // ترجع void
ملاحظة: يمكن لـ TypeScript غالباً استنتاج نوع الإرجاع من جسم الدالة، لكن من الممارسات الجيدة إضافة تعليقات أنواع الإرجاع صراحةً للحصول على توثيق أفضل واكتشاف الأخطاء.

المعاملات الاختيارية

المعاملات الاختيارية تسمح لك باستدعاء دالة دون تقديم جميع الوسائط. قم بتحديد معامل كاختياري عن طريق إضافة علامة استفهام (?) بعد اسم المعامل.

المعاملات الاختيارية:

// دالة مع معامل اختياري
function greet(name: string, greeting?: string): string {
  if (greeting) {
    return `${greeting}, ${name}!`;
  }
  return `Hello, ${name}!`;
}

// الاستخدام
console.log(greet('Alice'));              // "Hello, Alice!"
console.log(greet('Bob', 'Good morning')); // "Good morning, Bob!"

// معاملات اختيارية متعددة
function createUser(
  username: string,
  email?: string,
  age?: number
): object {
  return {
    username,
    email: email || 'N/A',
    age: age || 0
  };
}

const user1 = createUser('john_doe');
const user2 = createUser('jane_smith', 'jane@example.com');
const user3 = createUser('bob_jones', 'bob@example.com', 30);
تحذير: يجب أن تأتي المعاملات الاختيارية بعد المعاملات المطلوبة. لا يمكن أن يكون لديك معامل مطلوب بعد معامل اختياري.

المعاملات الافتراضية

المعاملات الافتراضية توفر قيمة احتياطية إذا لم يتم تمرير وسيط. على عكس المعاملات الاختيارية، لا تحتاج المعاملات الافتراضية إلى علامة ?.

المعاملات الافتراضية:

// دالة مع معامل افتراضي
function calculatePrice(
  price: number,
  taxRate: number = 0.1,
  discount: number = 0
): number {
  const taxAmount = price * taxRate;
  const discountAmount = price * discount;
  return price + taxAmount - discountAmount;
}

// الاستخدام
console.log(calculatePrice(100));           // يستخدم الافتراضيات: 110
console.log(calculatePrice(100, 0.15));     // ضريبة مخصصة: 115
console.log(calculatePrice(100, 0.1, 0.2)); // ضريبة وخصم مخصصان: 90

// المعاملات الافتراضية يمكن أن تشير إلى المعاملات السابقة
function buildUrl(
  protocol: string = 'https',
  domain: string,
  path: string = '/'
): string {
  return `${protocol}://${domain}${path}`;
}

console.log(buildUrl('http', 'example.com'));
// "http://example.com/"

console.log(buildUrl(undefined, 'example.com', '/about'));
// "https://example.com/about"
نصيحة: المعاملات الافتراضية تكون اختيارية تلقائياً. تستنتج TypeScript نوع المعامل من القيمة الافتراضية، لذلك لا تحتاج إلى تحديده صراحةً (على الرغم من أنه يمكنك ذلك للوضوح).

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

معاملات الباقي تسمح لدالة بقبول عدد غير محدد من الوسائط كمصفوفة. استخدم عامل الانتشار (...) مع تعليق نوع للمصفوفة.

معاملات الباقي:

// دالة مع معاملات الباقي
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

// الاستخدام
console.log(sum(1, 2, 3));          // 6
console.log(sum(10, 20, 30, 40));   // 100
console.log(sum());                 // 0

// معاملات الباقي مع معاملات أخرى
function createTeam(
  teamName: string,
  captain: string,
  ...members: string[]
): object {
  return {
    name: teamName,
    captain,
    members,
    totalMembers: members.length + 1 // +1 للقائد
  };
}

const team = createTeam(
  'Alpha Team',
  'John',
  'Alice',
  'Bob',
  'Charlie'
);

console.log(team);
// {
//   name: 'Alpha Team',
//   captain: 'John',
//   members: ['Alice', 'Bob', 'Charlie'],
//   totalMembers: 4
// }

// معاملات الباقي مع أنواع مختلفة
function logValues(prefix: string, ...values: (string | number)[]): void {
  values.forEach(value => {
    console.log(`${prefix}: ${value}`);
  });
}

logValues('Item', 'apple', 42, 'banana', 100);
ملاحظة: يجب أن تكون معاملات الباقي هي المعامل الأخير في توقيع الدالة. يمكن أن يكون لديك معامل باقي واحد فقط لكل دالة.

تعبيرات نوع الدالة

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

تعبيرات نوع الدالة:

// تعريف نوع الدالة
type MathOperation = (a: number, b: number) => number;

// دوال تطابق النوع
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
const divide: MathOperation = (a, b) => b !== 0 ? a / b : 0;

// دالة تقبل دالة أخرى
function calculate(
  a: number,
  b: number,
  operation: MathOperation
): number {
  return operation(a, b);
}

// الاستخدام
console.log(calculate(10, 5, add));      // 15
console.log(calculate(10, 5, subtract)); // 5
console.log(calculate(10, 5, multiply)); // 50
console.log(calculate(10, 5, divide));   // 2

// نوع دالة أكثر تعقيداً
type Validator = (input: string) => {
  isValid: boolean;
  errors: string[];
};

const emailValidator: Validator = (input) => {
  const errors: string[] = [];

  if (!input.includes('@')) {
    errors.push('Email must contain @');
  }

  if (input.length < 5) {
    errors.push('Email too short');
  }

  return {
    isValid: errors.length === 0,
    errors
  };
};

console.log(emailValidator('test@example.com')); // { isValid: true, errors: [] }
console.log(emailValidator('bad'));              // { isValid: false, errors: [...] }

تحميلات الدالة الزائدة

تحميلات الدالة الزائدة تسمح لك بتعريف توقيعات دالة متعددة لنفس الدالة. هذا مفيد عندما يمكن استدعاء دالة بأنواع معاملات مختلفة أو أعداد معاملات مختلفة.

تحميلات الدالة الزائدة:

// توقيعات التحميل الزائد
function formatDate(date: Date): string;
function formatDate(timestamp: number): string;
function formatDate(year: number, month: number, day: number): string;

// توقيع التنفيذ (يجب أن يكون متوافقاً مع جميع التحميلات الزائدة)
function formatDate(
  dateOrYear: Date | number,
  month?: number,
  day?: number
): string {
  if (dateOrYear instanceof Date) {
    // تم الاستدعاء بكائن Date
    return dateOrYear.toISOString().split('T')[0];
  } else if (month !== undefined && day !== undefined) {
    // تم الاستدعاء بالسنة والشهر واليوم
    return `${dateOrYear}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  } else {
    // تم الاستدعاء بالطابع الزمني
    return new Date(dateOrYear).toISOString().split('T')[0];
  }
}

// الاستخدام - TypeScript تفرض توقيعات التحميل الزائد
const date1 = formatDate(new Date());         // كائن Date
const date2 = formatDate(1707926400000);      // الطابع الزمني
const date3 = formatDate(2024, 2, 14);        // السنة والشهر واليوم

console.log(date1); // "2024-02-14"
console.log(date2); // "2024-02-14"
console.log(date3); // "2024-02-14"

// مثال آخر: دالة البحث
function search(query: string): string[];
function search(query: string, limit: number): string[];
function search(query: string, limit: number, offset: number): string[];

function search(
  query: string,
  limit?: number,
  offset?: number
): string[] {
  // محاكاة البحث في قاعدة البيانات
  const allResults = [
    `Result 1 for "${query}"`,
    `Result 2 for "${query}"`,
    `Result 3 for "${query}"`,
    `Result 4 for "${query}"`,
    `Result 5 for "${query}"`
  ];

  const start = offset || 0;
  const end = limit ? start + limit : allResults.length;

  return allResults.slice(start, end);
}

console.log(search('typescript'));        // جميع النتائج
console.log(search('typescript', 2));     // أول نتيجتين
console.log(search('typescript', 2, 2));  // نتيجتان تبدآن من الفهرس 2
تحذير: يجب أن يكون توقيع التنفيذ متوافقاً مع جميع توقيعات التحميل الزائد، لكنه غير مرئي للمستدعين. يتم استخدام توقيعات التحميل الزائد فقط للتحقق من الأنواع.

الدوال العامة

الدوال العامة تسمح لك بكتابة دوال تعمل مع أنواع متعددة مع الحفاظ على سلامة الأنواع:

الدوال العامة:

// دالة عامة مع معامل النوع
function identity<T>(value: T): T {
  return value;
}

// الاستخدام - يتم استنتاج النوع
const num = identity(42);          // T هو number
const str = identity('hello');     // T هو string
const arr = identity([1, 2, 3]);   // T هو number[]

// دالة عامة مع معاملات نوع متعددة
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const pair1 = pair('age', 30);          // [string, number]
const pair2 = pair(true, 'success');    // [boolean, string]

// دالة عامة مع قيود
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = {
  name: 'Alice',
  age: 30,
  email: 'alice@example.com'
};

const name = getProperty(person, 'name');   // string
const age = getProperty(person, 'age');     // number
// const invalid = getProperty(person, 'invalid'); // خطأ: مفتاح غير صالح

// عمليات المصفوفة العامة
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const numbers = [1, 2, 3, 4, 5];
const firstNum = first(numbers);  // number | undefined
const lastNum = last(numbers);    // number | undefined

const words = ['hello', 'world'];
const firstWord = first(words);   // string | undefined
const lastWord = last(words);     // string | undefined

نوع معامل this

تسمح لك TypeScript بإعلان نوع this في دالة:

نوع معامل this:

interface User {
  name: string;
  age: number;
  greet(this: User): void;
}

const user: User = {
  name: 'Alice',
  age: 30,
  greet() {
    // TypeScript تعرف أن 'this' هي User
    console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
  }
};

user.greet(); // "Hello, I'm Alice and I'm 30 years old"

// منع ربط this غير صحيح
const greetFunction = user.greet;
// greetFunction(); // خطأ: سياق 'this' غير قابل للتعيين

// استخدام this في دوال مستقلة
interface Database {
  host: string;
  port: number;
}

function connect(this: Database): string {
  return `Connecting to ${this.host}:${this.port}`;
}

const db: Database = {
  host: 'localhost',
  port: 5432
};

// الاستدعاء بالسياق الصحيح
console.log(connect.call(db)); // "Connecting to localhost:5432"

دوال الاستدعاء الراجعة

اكتب دوال الاستدعاء الراجعة بشكل صحيح لضمان سلامة الأنواع:

أنواع دوال الاستدعاء الراجعة:

// دالة تقبل استدعاء راجع
function processArray(
  items: number[],
  callback: (item: number, index: number) => void
): void {
  items.forEach((item, index) => {
    callback(item, index);
  });
}

// الاستخدام
processArray([1, 2, 3, 4, 5], (num, idx) => {
  console.log(`Item ${idx}: ${num * 2}`);
});

// استدعاء راجع مع قيمة إرجاع
function filterArray<T>(
  items: T[],
  predicate: (item: T) => boolean
): T[] {
  return items.filter(predicate);
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = filterArray(numbers, num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]

// استدعاء راجع غير متزامن
function fetchData(
  url: string,
  onSuccess: (data: unknown) => void,
  onError: (error: Error) => void
): void {
  fetch(url)
    .then(response => response.json())
    .then(data => onSuccess(data))
    .catch(error => onError(error));
}

fetchData(
  'https://api.example.com/data',
  (data) => console.log('Success:', data),
  (error) => console.error('Error:', error.message)
);

الدوال غير المتزامنة

الدوال غير المتزامنة في TypeScript ترجع نوع Promise:

أنواع الدوال غير المتزامنة:

// دالة غير متزامنة مع نوع إرجاع صريح
async function fetchUser(id: string): Promise<{
  id: string;
  name: string;
  email: string;
}> {
  // محاكاة استدعاء API
  await new Promise(resolve => setTimeout(resolve, 1000));

  return {
    id,
    name: 'John Doe',
    email: 'john@example.com'
  };
}

// استخدام الدالة غير المتزامنة
async function main() {
  try {
    const user = await fetchUser('123');
    console.log(user.name); // TypeScript تعرف شكل user
  } catch (error) {
    console.error('Failed to fetch user:', error);
  }
}

// دالة غير متزامنة عامة
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json() as T;
}

// الاستخدام مع معامل النوع
interface Post {
  id: number;
  title: string;
  body: string;
}

async function loadPost(id: number): Promise<Post> {
  return await fetchData<Post>(`https://api.example.com/posts/${id}`);
}
تمرين:
  1. أنشئ دالة calculateDiscount التي تأخذ price: number، اختيارية discountPercent: number (افتراضي 10)، واختيارية memberDiscount: number. أرجع السعر النهائي بعد تطبيق الخصومات.
  2. أنشئ دالة عامة groupBy التي تأخذ مصفوفة من العناصر ودالة محدد المفتاح، وترجع كائناً يجمع العناصر حسب المفتاح المحدد.
  3. أنشئ تحميلات زائدة للدالة getValue التي تقبل إما مفتاح سلسلة أو فهرس رقمي لاسترداد القيم من مصفوفة أو كائن.
  4. أنشئ دالة غير متزامنة fetchUserPosts التي تأخذ معرف المستخدم وترجع Promise لمصفوفة من المشاركات.
  5. اختبر جميع دوالك مع بيانات نموذجية.

الملخص

  • أنواع المعاملات وأنواع الإرجاع تجعل الدوال آمنة من حيث الأنواع وموثقة ذاتياً
  • المعاملات الاختيارية تستخدم ? ويجب أن تأتي بعد المعاملات المطلوبة
  • المعاملات الافتراضية توفر قيم احتياطية وتكون اختيارية تلقائياً
  • معاملات الباقي تستخدم ... لقبول عدد غير محدد من الوسائط كمصفوفة
  • تعبيرات نوع الدالة تسمح لك بتعريف أنواع دوال قابلة لإعادة الاستخدام
  • تحميلات الدالة الزائدة تعرف توقيعات استدعاء متعددة لنفس الدالة
  • الدوال العامة تعمل مع أنواع متعددة مع الحفاظ على سلامة الأنواع
  • نوع معامل this يضمن ربط السياق الصحيح في الدوال
  • الدوال غير المتزامنة ترجع أنواع Promise<T> تلقائياً