الدليل الشامل

توثيق تايب سكريبت

الدليل العربي الشامل للغة TypeScript من الأساسيات وحتى المفاهيم المتقدمة

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

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

مزايا استخدام تايب سكريبت

  • فحص الأنواع الثابتة: يساعد في اكتشاف الأخطاء أثناء التطوير.
  • دعم الواجهات البرمجية: يسمح بتعريف العقود (contracts) بين أجزاء الكود.
  • دعم الفئات والنماذج: يوفر بناء أكثر تنظيمًا للمشاريع الكبيرة.
  • الاقتراحات الذكية: تحسين تجربة المطور مع اقتراحات أفضل في بيئات التطوير.
  • التوافق مع جافا سكريبت: كل كود جافا سكريبت صالح هو أيضًا كود تايب سكريبت صالح.

مقارنة بين جافا سكريبت وتايب سكريبت:

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

تثبيت تايب سكريبت

يمكنك تثبيت تايب سكريبت باستخدام مدير الحزم npm (Node Package Manager). يتطلب ذلك تثبيت Node.js أولاً.

تثبيت تايب سكريبت عالميًا

npm install -g typescript

يمكنك التحقق من نجاح التثبيت والإصدار المثبت بالأمر التالي:

tsc --version

تثبيت تايب سكريبت في مشروع محدد

npm install --save-dev typescript

إنشاء مشروع تايب سكريبت جديد

بعد تثبيت TypeScript، يمكنك إنشاء ملف تكوين TypeScript (tsconfig.json) باستخدام الأمر التالي:

tsc --init

ترجمة ملف تايب سكريبت

tsc filename.ts

ملاحظة

عند ترجمة ملف TypeScript، سينتج ملف JavaScript بنفس الاسم ولكن بامتداد .js

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

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

boolean

يمثل قيمة منطقية (true أو false)

let isDone: boolean = false;

number

يمثل القيم العددية الصحيحة والعشرية

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;

string

يمثل النصوص والسلاسل النصية

let color: string = "blue";
let fullName: string = `Mohamed Ali`;

array

يمثل المصفوفات (هناك طريقتان للتعريف)

let list1: number[] = [1, 2, 3];
let list2: Array = [1, 2, 3];

tuple

يمثل مصفوفة بعدد معروف من العناصر ذات أنواع محددة

let x: [string, number];
x = ["hello", 10]; // صحيح
// x = [10, "hello"]; // خطأ

enum

مجموعة من الثوابت المسماة

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

any

يمثل أي نوع، يستخدم عند عدم معرفة النوع مسبقًا

let notSure: any = 4;
notSure = "يمكن أن تكون سلسلة نصية";
notSure = false; // أو قيمة منطقية

void

يمثل عدم وجود نوع، غالبًا يستخدم كنوع إرجاع للدوال

function warnUser(): void {
  console.log("هذا تحذير");
}

null و undefined

تمثل قيم null و undefined، وهي أنواع فرعية لجميع الأنواع الأخرى

let u: undefined = undefined;
let n: null = null;

never

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

function error(message: string): never {
  throw new Error(message);
}

object

يمثل قيمة غير أولية (non-primitive)

let obj: object = {
  name: "علي",
  age: 30
};

unknown

مثل any، لكن أكثر أمانًا لأنه يتطلب فحص النوع قبل الاستخدام

let value: unknown = "مرحبا";
if (typeof value === "string") {
  console.log(value.toUpperCase());
}

الاستنتاج التلقائي للنوع

تايب سكريبت يمكنه استنتاج نوع المتغير تلقائيًا بناءً على قيمته الأولية:

// لا حاجة لتحديد النوع، سيستنتج TypeScript أنه 'number'
let x = 10;

// سيستنتج أنه 'string'
let name = "أحمد";

المتغيرات

في تايب سكريبت، يمكن تعريف المتغيرات باستخدام الكلمات المفتاحية var، let، و const، مع إمكانية تحديد نوع البيانات بشكل واضح.

تعريف المتغيرات

// تعريف متغير مع تحديد النوع
let counter: number = 0;

// تعريف متغير مع استنتاج النوع تلقائيًا
let name = "علي"; // سيستنتج أنه من نوع string

// تعريف ثابت (لا يمكن تغيير قيمته)
const PI: number = 3.14159;

// تعريف متغير يمكن أن يحمل قيمًا من نوعين مختلفين
let id: number | string;
id = 101; // صحيح
id = "abc"; // صحيح

نطاق المتغيرات

// المتغيرات المعرفة باستخدام var لها نطاق الدالة
function varExample() {
  var x = 10;
  if (true) {
    var x = 20; // نفس المتغير
    console.log(x); // 20
  }
  console.log(x); // 20 (تغيرت قيمة x)
}

// المتغيرات المعرفة باستخدام let لها نطاق الكتلة
function letExample() {
  let x = 10;
  if (true) {
    let x = 20; // متغير مختلف
    console.log(x); // 20
  }
  console.log(x); // 10 (لم تتغير قيمة x الخارجية)
}

تحذير

يفضل استخدام let و const بدلاً من var في كود تايب سكريبت الحديث لتجنب مشاكل النطاق.

تعريف الأنواع المركبة

// تعريف مصفوفة
let numbers: number[] = [1, 2, 3, 4, 5];

// تعريف كائن
let user: { name: string, age: number } = {
  name: "سامي",
  age: 25
};

// تعريف كائن باستخدام النوع المسجل مسبقًا (type)
type User = {
  name: string;
  age: number;
  email?: string; // الخاصية اختيارية (؟)
};

let admin: User = {
  name: "خالد",
  age: 30
};

الواجهات (Interfaces)

الواجهات في تايب سكريبت تحدد العقود التي يجب على الكائنات اتباعها. وهي طريقة لتعريف "شكل" الكائن.

تعريف الواجهة الأساسية

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

// استخدام الواجهة
let user: Person = {
  firstName: "أحمد",
  lastName: "محمد",
  age: 28
};

الخصائص الاختيارية

interface Vehicle {
  brand: string;
  model: string;
  year: number;
  color?: string; // خاصية اختيارية
}

// استخدام الواجهة مع خاصية اختيارية
let car: Vehicle = {
  brand: "تويوتا",
  model: "كامري",
  year: 2020
  // يمكن حذف color لأنها اختيارية
};

الخصائص للقراءة فقط

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

let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // خطأ! لا يمكن تعديل قيمة الخاصية للقراءة فقط

توسيع الواجهات

interface Animal {
  name: string;
  age: number;
}

interface Cat extends Animal {
  breed: string;
  meow(): void;
}

// استخدام الواجهة الموسعة
let cat: Cat = {
  name: "قطقوط",
  age: 3,
  breed: "سيامي",
  meow() {
    console.log("مياو!");
  }
};

واجهات الدوال

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src, sub) {
  return src.indexOf(sub) > -1;
};

تعريف الفهارس (Indexable Types)

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray = ["أبجد", "هوز", "حطي"];
let myStr: string = myArray[0];

الفئات (Classes)

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

تعريف الفئة الأساسية

class Person {
  name: string;
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  greet(): void {
    console.log(`مرحباً، اسمي ${this.name} وعمري ${this.age} سنة.`);
  }
}

// إنشاء كائن من الفئة
const person = new Person("أحمد", 30);
person.greet(); // مرحباً، اسمي أحمد وعمري 30 سنة.

الوصول (Access Modifiers)

توفر تايب سكريبت ثلاثة معدلات وصول للخصائص والطرق في الفئات:

class Employee {
  public name: string;      // يمكن الوصول إليها من أي مكان
  private salary: number;   // يمكن الوصول إليها فقط من داخل الفئة
  protected department: string; // يمكن الوصول إليها من داخل الفئة والفئات الوراثية
  
  constructor(name: string, salary: number, department: string) {
    this.name = name;
    this.salary = salary;
    this.department = department;
  }
  
  public displayInfo(): void {
    console.log(`الاسم: ${this.name}, القسم: ${this.department}`);
    this.displaySalary(); // يمكن استدعاء الطرق الخاصة من داخل الفئة
  }
  
  private displaySalary(): void {
    console.log(`الراتب: ${this.salary}`);
  }
}

const emp = new Employee("محمد", 5000, "هندسة");
emp.displayInfo(); // الاسم: محمد, القسم: هندسة
// emp.salary; // خطأ: لا يمكن الوصول للخاصية الخاصة
// emp.displaySalary(); // خطأ: لا يمكن استدعاء الطريقة الخاصة

الوراثة (Inheritance)

class Animal {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  move(distance: number = 0): void {
    console.log(`${this.name} تحرك ${distance} متر.`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name); // استدعاء منشئ الفئة الأب
  }
  
  bark(): void {
    console.log("هاو هاو!");
  }
  
  // تجاوز الطريقة في الفئة الأب
  move(distance: number = 5): void {
    console.log("يركض...");
    super.move(distance); // استدعاء طريقة الفئة الأب
  }
}

const dog = new Dog("ريكس");
dog.bark(); // هاو هاو!
dog.move(10); // يركض... ريكس تحرك 10 متر.

الخصائص والطرق الثابتة

class MathHelper {
  // خاصية ثابتة
  static PI: number = 3.14159;
  
  // طريقة ثابتة
  static calculateCircleArea(radius: number): number {
    return MathHelper.PI * radius * radius;
  }
}

// يمكن استخدام الخصائص والطرق الثابتة مباشرة دون إنشاء كائن
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.calculateCircleArea(5)); // 78.53975

الفئات المجردة (Abstract Classes)

abstract class Shape {
  color: string;
  
  constructor(color: string) {
    this.color = color;
  }
  
  abstract calculateArea(): number; // طريقة مجردة - يجب تنفيذها في الفئات الوراثية
  
  displayColor(): void { // طريقة عادية
    console.log(`لون الشكل هو ${this.color}`);
  }
}

class Circle extends Shape {
  radius: number;
  
  constructor(color: string, radius: number) {
    super(color);
    this.radius = radius;
  }
  
  // تنفيذ الطريقة المجردة
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

// لا يمكن إنشاء كائن من فئة مجردة
// const shape = new Shape("أحمر"); // خطأ!

const circle = new Circle("أزرق", 5);
circle.displayColor(); // لون الشكل هو أزرق
console.log(circle.calculateArea()); // 78.53981633974483

الخاصيات (Getters & Setters)

class Product {
  private _price: number = 0;
  
  get price(): number {
    return this._price;
  }
  
  set price(value: number) {
    if (value < 0) {
      throw new Error("السعر لا يمكن أن يكون سالبًا");
    }
    this._price = value;
  }
}

const product = new Product();
product.price = 100; // استدعاء الـ setter
console.log(product.price); // 100 - استدعاء الـ getter

// product.price = -10; // خطأ: السعر لا يمكن أن يكون سالبًا

الدوال (Functions)

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

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

// دالة بسيطة مع تحديد نوع المعاملات ونوع القيمة المرجعة
function add(x: number, y: number): number {
  return x + y;
}

// تعبير الدالة (Function Expression)
const multiply = function(x: number, y: number): number {
  return x * y;
};

// السهم الدالة (Arrow Function)
const divide = (x: number, y: number): number => x / y;

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

// معامل اختياري (بإضافة علامة ?)
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  } else {
    return firstName;
  }
}

console.log(buildName("أحمد")); // أحمد
console.log(buildName("أحمد", "محمد")); // أحمد محمد

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

// معامل بقيمة افتراضية
function greet(name: string, greeting: string = "مرحبًا"): string {
  return `${greeting}, ${name}!`;
}

console.log(greet("علي")); // مرحبًا, علي!
console.log(greet("خالد", "أهلاً")); // أهلاً, خالد!

معاملات الدالة المتبقية (Rest Parameters)

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

console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15

أنواع الدوال (Function Types)

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

// متغير من نوع دالة
let operation: MathOperation;

// تعيين دالة متوافقة مع النوع
operation = (a, b) => a + b;
console.log(operation(5, 3)); // 8

// يمكن استخدام نوع الدالة كمعامل
function applyOperation(x: number, y: number, op: MathOperation): number {
  return op(x, y);
}

console.log(applyOperation(10, 5, (a, b) => a * b)); // 50

الدوال المتحملة لـ this

interface Person {
  name: string;
  greet(this: Person): void;
}

const person: Person = {
  name: "أحمد",
  greet() {
    console.log(`مرحباً، اسمي ${this.name}`);
  }
};

person.greet(); // مرحباً، اسمي أحمد

// تايب سكريبت سيتحقق من استخدام this بشكل صحيح
// const badGreet = person.greet; // إذا تم استدعاء هذا، سيظهر خطأ عند التشغيل

الدوال المتحملة (Overloading)

// تعريف عدة توقيعات للدالة
function processInput(input: string): string;
function processInput(input: number): number;
function processInput(input: boolean): boolean;

// التنفيذ الفعلي للدالة
function processInput(input: string | number | boolean): string | number | boolean {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else if (typeof input === "number") {
    return input * 2;
  } else {
    return !input;
  }
}

console.log(processInput("hello")); // "HELLO"
console.log(processInput(10)); // 20
console.log(processInput(true)); // false

العموميات (Generics)

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

دالة عمومية

// دالة عمومية مع نوع T
function identity(arg: T): T {
  return arg;
}

// استخدام الدالة العمومية مع تحديد النوع صراحة
let result1 = identity("مرحبًا");

// أو ترك تايب سكريبت ليستنتج النوع تلقائيًا
let result2 = identity(42); // سيستنتج أن T هي number

واجهة عمومية

// واجهة عمومية
interface GenericIdentity {
  (arg: T): T;
}

// تنفيذ الواجهة
let myIdentity: GenericIdentity = (arg) => arg;

فئة عمومية

// فئة عمومية
class Box {
  private content: T;
  
  constructor(value: T) {
    this.content = value;
  }
  
  getValue(): T {
    return this.content;
  }
}

// إنشاء مثيلات للفئة مع أنواع مختلفة
const numberBox = new Box(123);
console.log(numberBox.getValue()); // 123

const stringBox = new Box("مرحبًا");
console.log(stringBox.getValue()); // "مرحبًا"

قيود العموميات

// قيد العمومية باستخدام extends
interface Lengthwise {
  length: number;
}

// دالة عمومية مع قيد: T يجب أن تنفذ واجهة Lengthwise
function getLength(arg: T): number {
  return arg.length;
}

// يمكن استدعاؤها مع أنواع تحتوي على خاصية length
console.log(getLength("مرحبًا")); // 5 (النص له خاصية length)
console.log(getLength([1, 2, 3])); // 3 (المصفوفة لها خاصية length)

// وهذا سيسبب خطأ في وقت التحويل البرمجي
// console.log(getLength(123)); // خطأ: الرقم ليس له خاصية length

استخدام عدة أنواع عمومية

// دالة مع عدة أنواع عمومية
function merge(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const result = merge({ name: "أحمد" }, { age: 30 });
console.log(result); // { name: "أحمد", age: 30 }
console.log(result.name); // "أحمد"
console.log(result.age); // 30

العموميات مع قيود متعددة

// تعريف واجهات للقيود
interface Printable {
  print(): void;
}

interface Loggable {
  log(): void;
}

// دالة مع نوع عمومي مقيد بعدة واجهات
function createObject(factory: () => T): T {
  const obj = factory();
  obj.print();
  obj.log();
  return obj;
}

// استخدام الدالة
class PrintableLoggable implements Printable, Loggable {
  print() {
    console.log("طباعة...");
  }
  log() {
    console.log("تسجيل...");
  }
}

const obj = createObject(() => new PrintableLoggable());

التعدادات (Enums)

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

تعدادات رقمية

// تعداد رقمي بسيط
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

// استخدام التعداد
let dir: Direction = Direction.Down;
console.log(dir); // 1

// يمكن أيضًا الوصول للقيمة عن طريق الاسم
console.log(Direction[2]); // "Left"

تعدادات مع قيم مخصصة

// تعداد مع قيم بداية مخصصة
enum StatusCode {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  ServerError = 500
}

// استخدام التعداد
function handleResponse(status: StatusCode) {
  switch (status) {
    case StatusCode.OK:
      console.log("تمت العملية بنجاح");
      break;
    case StatusCode.NotFound:
      console.log("الصفحة غير موجودة");
      break;
    default:
      console.log("حالة غير معروفة: " + status);
  }
}

handleResponse(StatusCode.OK); // تمت العملية بنجاح

تعدادات نصية

// تعداد نصي
enum MediaType {
  JSON = "application/json",
  XML = "application/xml",
  FORM = "application/x-www-form-urlencoded"
}

// استخدام التعداد النصي
function setContentType(type: MediaType) {
  console.log(`Content-Type: ${type}`);
}

setContentType(MediaType.JSON); // Content-Type: application/json

تعدادات الثوابت والحسابية

// تعدادات ثوابت وحسابية
enum FileAccess {
  // ثوابت
  None = 0,
  Read = 1,
  Write = 2,
  
  // قيم حسابية
  ReadWrite = Read | Write,
  All = ReadWrite | 4
}

// استخدام التعداد
let access = FileAccess.Read | FileAccess.Write;
console.log(access); // 3
console.log(access === FileAccess.ReadWrite); // true

// استخدام العمليات المنطقية للتحقق
function checkAccess(access: FileAccess): void {
  if (access & FileAccess.Read) {
    console.log("لديك صلاحية القراءة");
  }
  if (access & FileAccess.Write) {
    console.log("لديك صلاحية الكتابة");
  }
}

checkAccess(FileAccess.ReadWrite);
// لديك صلاحية القراءة
// لديك صلاحية الكتابة

التعدادات التي يمكن تمديدها

// التعداد المعرّف في ملفين مختلفين
enum Colors {
  Red = 1,
  Green,
  Blue
}

// يمكن استكمال التعداد
enum Colors {
  Yellow = 10,
  Purple,
  Orange
}

// استخدام التعداد
console.log(Colors.Green); // 2
console.log(Colors.Purple); // 11

التعدادات const (أكثر كفاءة)

// تعداد const يولد كود أكثر كفاءة عند التحويل
const enum Day {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

// استخدام التعداد
let weekend = [Day.Saturday, Day.Sunday];

نصيحة مفيدة

استخدم const enum عندما لا تحتاج إلى الوصول إلى قيم التعداد عن طريق اسم المفتاح (مثل Day[0])، لأنه ينتج كود أكثر كفاءة.

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

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

تأكيدات النوع الأساسية

// صيغة "as"
let someValue: any = "هذا نص";
let strLength: number = (someValue as string).length;

// صيغة علامات زاوية (أقل استخدامًا وغير مدعومة في JSX)
let otherValue: any = "نص آخر";
let otherLength: number = (<string>otherValue).length;

تأكيدات النوع للكائنات

interface User {
  name: string;
  email: string;
}

// بيانات من مصدر خارجي (مثل استجابة API)
const data: any = {
  name: "أحمد",
  email: "[email protected]",
  // قد تحتوي على خصائص أخرى غير معلنة في الواجهة
};

// تأكيد نوع الكائن
const user = data as User;

// الآن يمكن الوصول إلى خصائص المستخدم بأمان النوع
console.log(user.name); // أحمد

التأكيد المزدوج عبر unknown

// تحويل بين نوعين غير متوافقين
let value: string = "42";

// التحويل المباشر سيسبب خطأ
// let num: number = value as number; // خطأ!

// التحويل المزدوج باستخدام unknown
let num: number = (value as unknown) as number;

تحذير

استخدم تأكيدات النوع بحذر! أنت تخبر مترجم TypeScript بتجاوز فحص النوع، مما قد يؤدي إلى أخطاء في وقت التشغيل إذا كان تأكيدك غير صحيح.

فحوصات النوع في وقت التشغيل

// بدلاً من تأكيدات النوع، يمكن استخدام فحوصات النوع
function example(value: any) {
  // استخدام typeof للأنواع الأساسية
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
  
  // استخدام instanceof للكائنات
  if (value instanceof Date) {
    console.log(value.toISOString());
  }
}

// في حالة الواجهات، يمكن فحص وجود خاصية
function isUser(obj: any): obj is User {
  return obj && typeof obj.name === "string" && typeof obj.email === "string";
}

function processUser(input: any) {
  if (isUser(input)) {
    // الآن تايب سكريبت يعرف أن input هو User
    console.log(`اسم المستخدم: ${input.name}`);
  } else {
    console.log("البيانات غير صالحة");
  }
}

حواسم النوع المضمنة

// non-null assertion operator (!)
function process(value: string | null | undefined) {
  // استخدام ! لإخبار المترجم أن القيمة لن تكون null أو undefined
  let length: number = value!.length;
}

// definite assignment assertion (!)
let name!: string; // إخبار المترجم أن المتغير سيتم تعيينه قبل استخدامه
initialize();
console.log(name.length); // لن يتسبب بخطأ

function initialize() {
  name = "أحمد";
}

الأنواع المتقدمة

تايب سكريبت يوفر العديد من الميزات المتقدمة للتعامل مع الأنواع، والتي تسمح بإنشاء أنظمة أنواع معقدة ومرنة.

الأنواع الاتحادية (Union Types)

// يمكن للمتغير أن يتقبل قيمة من نوعين أو أكثر
let id: string | number;
id = 101; // صحيح
id = "abc"; // صحيح
// id = true; // خطأ

// يمكن أيضًا استخدامها في المعاملات
function processId(id: string | number) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id * 2);
  }
}

الأنواع التقاطعية (Intersection Types)

// الأنواع التقاطعية تجمع خصائص نوعين أو أكثر
interface User {
  id: number;
  name: string;
}

interface Employee {
  companyId: number;
  role: string;
}

// نوع جديد يجمع خصائص النوعين
type EmployeeUser = User & Employee;

// يجب أن تشتمل الكائنات على خصائص كلا النوعين
const empUser: EmployeeUser = {
  id: 1,
  name: "سامي",
  companyId: 101,
  role: "مطور"
};

الأنواع المُحرَسة (Type Guards)

// الأنواع المحرسة تساعد في تضييق نطاق النوع
function isPerson(obj: any): obj is { name: string; age: number } {
  return obj && typeof obj.name === "string" && typeof obj.age === "number";
}

function greet(obj: any) {
  if (isPerson(obj)) {
    // الآن يعرف TypeScript أن obj له خصائص name و age
    console.log(`مرحبًا ${obj.name}, عمرك ${obj.age} سنة`);
  } else {
    console.log("مرحبًا غريب");
  }
}

// باستخدام typeof و instanceof
function process(value: string | number | Date) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (typeof value === "number") {
    console.log(value.toFixed(2));
  } else if (value instanceof Date) {
    console.log(value.toISOString());
  }
}

الأنواع المتميزة (Discriminated Unions)

// الأنواع المتميزة تستخدم خاصية مشتركة للتمييز
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

// اتحاد الأنواع
type Shape = Circle | Square;

// استخدام خاصية kind للتمييز
function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // TypeScript يعرف هنا أن shape هو Circle
      return Math.PI * shape.radius ** 2;
    case "square":
      // TypeScript يعرف هنا أن shape هو Square
      return shape.sideLength ** 2;
  }
}

أنواع المفاتيح (Keyof Type Operator)

interface User {
  id: number;
  name: string;
  email: string;
}

// keyof User سيكون "id" | "name" | "email"
function getProperty(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = {
  id: 1,
  name: "محمد",
  email: "[email protected]"
};

const userName = getProperty(user, "name"); // محمد
// const invalid = getProperty(user, "age"); // خطأ: "age" ليس مفتاحًا في User

أنواع المهام (Mapped Types)

// أنواع المهام تنشئ أنواعًا جديدة بناءً على أنواع موجودة
interface Person {
  name: string;
  age: number;
}

// جعل كل الخصائص اختيارية
type PartialPerson = {
  [K in keyof Person]?: Person[K];
};

// جعل كل الخصائص للقراءة فقط
type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

// تحويل نوع كل خاصية
type PersonWithStringValues = {
  [K in keyof Person]: string;
};

أنواع شرطية (Conditional Types)

// الأنواع الشرطية تعتمد على علاقة بين الأنواع
type IsNumber = T extends number ? "yes" : "no";

type WithNumber = IsNumber; // "yes"
type WithString = IsNumber; // "no"

// مثال أكثر فائدة: استخراج نوع العنصر من مصفوفة
type ElementType = T extends (infer U)[] ? U : never;

type NumberArray = number[];
type ExtractedType = ElementType; // number

أنواع محددة مسبقًا

// أنواع مفيدة متوفرة في TypeScript
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

// Partial - يجعل كل الخصائص اختيارية
type PartialUser = Partial;
// مثل: { id?: number; name?: string; email?: string; role?: "admin" | "user"; }

// Required - يجعل كل الخصائص مطلوبة
type RequiredUser = Required;

// Readonly - يجعل كل الخصائص للقراءة فقط
type ReadonlyUser = Readonly;

// Pick - يختار خصائص محددة
type UserCredentials = Pick;
// مثل: { email: string; role: "admin" | "user"; }

// Omit - يستبعد خصائص محددة
type PublicUser = Omit;
// مثل: { name: string; role: "admin" | "user"; }

// ReturnType - يحصل على نوع القيمة المرجعة من الدالة
function createUser() { return { name: "أحمد", age: 30 }; }
type UserObject = ReturnType;
// مثل: { name: string; age: number; }

الوحدات (Modules)

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

تصدير (Export)

// math.ts - ملف يصدّر دوال وثوابت

// تصدير ثابت
export const PI = 3.14159;

// تصدير دالة
export function add(x: number, y: number): number {
  return x + y;
}

// تصدير كلاس
export class Calculator {
  multiply(x: number, y: number): number {
    return x * y;
  }
}

// تصدير نوع أو واجهة
export interface Shape {
  area(): number;
}

// تعريف دالة بدون تصديرها مباشرة
function subtract(x: number, y: number): number {
  return x - y;
}

// تصدير الدالة لاحقًا
export { subtract };

// تصدير مع إعادة تسمية
export { subtract as minus };

استيراد (Import)

// app.ts - ملف يستورد من math.ts

// استيراد عناصر محددة
import { add, PI } from './math';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

// استيراد مع إعادة تسمية
import { Calculator as Calc } from './math';
const calc = new Calc();
console.log(calc.multiply(2, 3)); // 6

// استيراد كل الصادرات ككائن
import * as MathUtils from './math';
console.log(MathUtils.subtract(5, 2)); // 3
console.log(MathUtils.PI); // 3.14159

// استيراد أنواع
import { Shape } from './math';
class Circle implements Shape {
  radius: number;
  
  constructor(radius: number) {
    this.radius = radius;
  }
  
  area(): number {
    return MathUtils.PI * this.radius * this.radius;
  }
}

الصادرات الافتراضية (Default Exports)

// user.ts - ملف به تصدير افتراضي
export default class User {
  constructor(public name: string, public age: number) {}
  
  greet() {
    return `مرحبًا، اسمي ${this.name}`;
  }
}

// استيراد الصادرات الافتراضية
// profile.ts
import User from './user';
const user = new User("أحمد", 30);
console.log(user.greet()); // مرحبًا، اسمي أحمد

إعادة التصدير (Re-exporting)

// index.ts - ملف يجمع صادرات من ملفات متعددة

// إعادة تصدير كل الصادرات
export * from './math';
export * from './user';

// إعادة تصدير عناصر محددة
export { add, subtract } from './math';

// إعادة تصدير مع إعادة تسمية
export { default as UserClass } from './user';

Import Type

// استيراد أنواع فقط بدون أي تنفيذ للكود
import type { Shape, Calculator } from './math';

// يمكن أيضاً استيراد أنواع وقيم معًا
import { add, type Shape } from './math';

دعم Dynamic Import

// استيراد ديناميكي (Lazy Loading) للوحدات
async function loadModule() {
  try {
    // سينتظر تحميل الوحدة
    const math = await import('./math');
    console.log(math.add(2, 3)); // 5
  } catch (error) {
    console.error("فشل تحميل الوحدة", error);
  }
}

ملاحظة

يدعم تايب سكريبت أنظمة وحدات مختلفة: ES Modules, CommonJS, AMD, UMD, وغيرها. يمكن تحديد نظام الوحدات المستخدم في ملف التكوين tsconfig.json.

مساحات الأسماء

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

معلومة

في مشاريع تايب سكريبت الحديثة، يُفضل استخدام الوحدات (Modules) على مساحات الأسماء (Namespaces). ومع ذلك، قد تجد مساحات الأسماء في مشاريع قديمة.

تعريف مساحة الأسماء

// تعريف مساحة أسماء
namespace Validation {
  // واجهة داخلية
  export interface StringValidator {
    isValid(s: string): boolean;
  }
  
  // فئة داخلية مصدرة
  export class EmailValidator implements StringValidator {
    isValid(s: string): boolean {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(s);
    }
  }
  
  // دالة داخلية مصدرة
  export function validate(validator: StringValidator, value: string): boolean {
    return validator.isValid(value);
  }
  
  // متغير داخلي غير مصدر (لا يمكن الوصول إليه خارج مساحة الأسماء)
  const defaultEmail = "[email protected]";
}

استخدام مساحة الأسماء

// استخدام مساحة الأسماء
let emailValidator = new Validation.EmailValidator();
let isValid = Validation.validate(emailValidator, "[email protected]");
console.log(isValid); // true

// استخدام الواجهة المصدرة
class PhoneValidator implements Validation.StringValidator {
  isValid(s: string): boolean {
    const phoneRegex = /^\d{10}$/;
    return phoneRegex.test(s);
  }
}

مساحات الأسماء المتداخلة

// مساحات أسماء متداخلة
namespace Utils {
  export namespace Validation {
    export class DateValidator {
      isValid(date: Date): boolean {
        return !isNaN(date.getTime());
      }
    }
  }
  
  export namespace Formatting {
    export function formatDate(date: Date): string {
      return date.toLocaleDateString('ar-EG');
    }
  }
}

// استخدام مساحات الأسماء المتداخلة
const dateValidator = new Utils.Validation.DateValidator();
const date = new Date();
if (dateValidator.isValid(date)) {
  console.log(Utils.Formatting.formatDate(date));
}

اختصارات مساحات الأسماء

// إنشاء اختصار لمساحة الأسماء
import Validators = Utils.Validation;
const myDateValidator = new Validators.DateValidator();

تقسيم مساحات الأسماء عبر الملفات

يمكن تقسيم مساحة الأسماء عبر عدة ملفات، ويمكن جمعها باستخدام التضمين المرجعي (/// <reference path="..." />).

// validators.ts
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
}

// email-validator.ts
/// <reference path="validators.ts" />
namespace Validation {
  export class EmailValidator implements StringValidator {
    isValid(s: string): boolean {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(s);
    }
  }
}

// استخدام الفئات من المساحة نفسها
// main.ts
/// <reference path="validators.ts" />
/// <reference path="email-validator.ts" />

let validator = new Validation.EmailValidator();

مراحل تحويل مساحات الأسماء

عند ترجمة مساحات الأسماء، يمكن تحديد نمط التحويل باستخدام خيار --outFile للجمع أو --module لتحويلها إلى وحدات.

# تجميع عدة ملفات في ملف واحد مع مساحات الأسماء
tsc --outFile output.js file1.ts file2.ts

المزخرفات (Decorators)

المزخرفات هي ميزة تجريبية في تايب سكريبت (وأصبحت قياسية في ECMAScript) تسمح بإضافة وظائف إضافية إلى الفئات والطرق والخصائص والمعاملات المعرفة.

ملاحظة

لاستخدام المزخرفات، يجب تفعيل خيار experimentalDecorators في ملف tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

مزخرف الفئة (Class Decorator)

// مزخرف فئة بسيط
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

// استخدام المزخرف
@sealed
class Person {
  constructor(public name: string) {}
}

// مزخرف فئة مع معاملات
function logger(logString: string) {
  return function(constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

@logger('تسجيل عن فئة الموظف')
class Employee {
  constructor(public name: string, public department: string) {}
}

مزخرف الطريقة (Method Decorator)

// مزخرف طريقة
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // الدالة الأصلية
  const originalMethod = descriptor.value;

  // تعديل الدالة لإضافة السجل
  descriptor.value = function(...args: any[]) {
    console.log(`استدعاء ${propertyKey} مع المعاملات: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} أرجع: ${JSON.stringify(result)}`);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  @log
  add(x: number, y: number) {
    return x + y;
  }
}

مزخرف الخاصية (Property Decorator)

// مزخرف خاصية
function defaultValue(value: any) {
  return function(target: any, propertyKey: string) {
    let val = value;
    
    // تعريف الواصف (getter/setter)
    const getter = function() {
      return val;
    };
    
    const setter = function(newVal: any) {
      val = newVal;
    };
    
    // استبدال الخاصية بواصف جديد
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Product {
  @defaultValue(0)
  price: number;
  
  constructor(public name: string) {}
}

مزخرف المعامل (Parameter Decorator)

// مزخرف معامل
function required(target: Object, propertyKey: string, parameterIndex: number) {
  // احفظ المعلمات المطلوبة
  const requiredParams: number[] = Reflect.getOwnMetadata('required', target, propertyKey) || [];
  requiredParams.push(parameterIndex);
  Reflect.defineMetadata('required', requiredParams, target, propertyKey);
}

// مزخرف لفحص المعاملات المطلوبة
function validate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const requiredParams: number[] = Reflect.getOwnMetadata('required', target, propertyName) || [];
    
    for (const index of requiredParams) {
      if (args[index] === undefined || args[index] === null) {
        throw new Error(`المعامل في الموضع ${index} مطلوب للدالة ${propertyName}`);
      }
    }
    
    return method.apply(this, args);
  };
  
  return descriptor;
}

class User {
  @validate
  updateProfile(@required name: string, email: string, @required age: number) {
    console.log(`تحديث الملف الشخصي: ${name}, ${email}, ${age}`);
  }
}

مزخرف الواصف (Accessor Decorator)

// مزخرف الواصف
function enumerable(value: boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

class Person {
  private _name: string;
  
  constructor(name: string) {
    this._name = name;
  }
  
  @enumerable(false)
  get name() {
    return this._name;
  }
  
  set name(value: string) {
    this._name = value;
  }
}

تسلسل تطبيق المزخرفات

// عند وجود عدة مزخرفات، يكون ترتيب التنفيذ كالتالي:
// 1. مزخرفات المعامل (من اليسار إلى اليمين)
// 2. مزخرفات الطريقة
// 3. مزخرفات الواصف (getter/setter)
// 4. مزخرفات الخاصية
// 5. مزخرفات الفئة

// مثال على تسلسل المزخرفات
function first() {
  console.log("أولاً");
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("تنفيذ المزخرف الأول");
  };
}

function second() {
  console.log("ثانيًا");
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("تنفيذ المزخرف الثاني");
  };
}

class Example {
  @first()
  @second()
  method() {}
}

// النتيجة:
// ثانيًا
// أولاً
// تنفيذ المزخرف الثاني
// تنفيذ المزخرف الأول

ملف التكوين (tsconfig.json)

ملف tsconfig.json يحدد خيارات المترجم ومعلومات المشروع لمشروع تايب سكريبت. يساعد على توحيد إعدادات التحويل البرمجي وضمان سلوك متسق.

إنشاء ملف التكوين

# إنشاء ملف تكوين جديد
tsc --init

محتوى ملف التكوين الأساسي

{
  "compilerOptions": {
    "target": "es2016",          /* تحديد إصدار ECMAScript المستهدف */
    "module": "commonjs",        /* تحديد نظام الوحدات: 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "outDir": "./dist",          /* تحديد مجلد الإخراج */
    "rootDir": "./src",          /* تحديد مجلد المصدر */
    "strict": true,              /* تفعيل جميع فحوصات النوع الصارمة */
    "esModuleInterop": true,     /* تفعيل التوافق بين أنظمة الوحدات المختلفة */
    "skipLibCheck": true,        /* تخطي فحص نوع ملفات .d.ts */
    "forceConsistentCasingInFileNames": true /* ضمان اتساق حالة أحرف أسماء الملفات */
  },
  "include": ["src/**/*"],       /* أنماط الملفات المراد تضمينها */
  "exclude": ["node_modules", "**/*.spec.ts"] /* أنماط الملفات المراد استبعادها */
}

خيارات المترجم الشائعة

الخيار الوصف
target تحديد إصدار JavaScript المراد إنتاجه (es5, es6, es2016, es2020, esnext)
module نظام الوحدات المستخدم (commonjs, amd, es2015, esnext)
outDir مسار مجلد الإخراج للملفات المترجمة
rootDir مسار مجلد المصدر للملفات
strict تفعيل جميع خيارات الصرامة في النوع
lib المكتبات المدمجة التي سيتم تضمينها (مثل dom, es2018)
declaration إنشاء ملفات .d.ts التي تصف الأنواع
sourceMap إنشاء ملفات sourceMap للتصحيح
noImplicitAny noImplicitAny رفض التعبيرات التي يستنتج لها المترجم النوع any
strictNullChecks التعامل مع null و undefined كنوع منفصل
noUnusedLocals إظهار خطأ للمتغيرات المحلية غير المستخدمة
noUnusedParameters إظهار خطأ لمعاملات الدوال غير المستخدمة
allowJs السماح بمعالجة ملفات جافا سكريبت
jsx كيفية معالجة ملفات JSX (preserve, react)
experimentalDecorators تفعيل دعم المزخرفات

إعدادات المشروع

{
  "compilerOptions": {
    // خيارات المترجم
  },
  "include": [
    "src/**/*"   // تضمين جميع الملفات في مجلد src
  ],
  "exclude": [
    "node_modules",  // استبعاد مجلد node_modules
    "**/*.test.ts"   // استبعاد ملفات الاختبار
  ],
  "files": [
    "core.ts",       // تضمين ملفات محددة فقط
    "utils.ts"
  ],
  "extends": "./tsconfig.base.json" // توريث الإعدادات من ملف آخر
}

استخدام مسارات مخصصة

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/app/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

تكوينات حسب البيئة

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

# هيكل الملفات
tsconfig.json           # التكوين الأساسي
tsconfig.dev.json       # تكوين بيئة التطوير
tsconfig.prod.json      # تكوين بيئة الإنتاج

# استخدام ملف تكوين محدد
tsc -p tsconfig.prod.json

أفضل الممارسات

إليك مجموعة من أفضل الممارسات لكتابة كود تايب سكريبت أفضل وأكثر أمانًا واحترافية.

استخدام الوضع الصارم (Strict Mode)

// في ملف tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

يفعل هذا الخيار مجموعة من الفحوصات الصارمة التي تساعد في اكتشاف المزيد من الأخطاء المحتملة.

تجنب استخدام any

// بدلاً من
function getData(data: any) {
  return data.name;
}

// استخدم
interface Data {
  name: string;
  [key: string]: any; // للخصائص الإضافية إذا لزم الأمر
}

function getData(data: Data) {
  return data.name;
}

استخدام واجهات أو أنواع

// تعريف بنية البيانات باستخدام واجهات أو أنواع
interface User {
  id: number;
  name: string;
  email: string;
}

// أو باستخدام type
type User = {
  id: number;
  name: string;
  email: string;
};

// استخدام التعريف
function sendEmail(user: User) {
  console.log(`إرسال بريد إلى ${user.name} على العنوان ${user.email}`);
}

استخدام تعريفات الأنواع الموجزة

// الاعتماد على استنتاج النوع عندما يكون واضحًا
// بدلاً من
const numbers: number[] = [1, 2, 3];
const result: number = add(1, 2);

// استخدم
const numbers = [1, 2, 3]; // يستنتج أنه number[]
const result = add(1, 2);  // يستنتج أنه number إذا كانت الدالة تحدد نوع الإرجاع

استخدام readonly للبيانات التي لا تتغير

// استخدام readonly للخصائص والمصفوفات
interface Config {
  readonly apiKey: string;
  readonly baseUrl: string;
}

function processData(data: readonly number[]) {
  // لا يمكن تعديل data داخل الدالة
  // data.push(1); // خطأ!
}

استخدام الأنواع المتميزة (Discriminated Unions)

// استخدام الأنواع المتميزة للتعامل مع الحالات المختلفة
type Success = {
  status: 'success';
  data: string[];
};

type Error = {
  status: 'error';
  message: string;
};

type Response = Success | Error;

function handleResponse(response: Response) {
  if (response.status === 'success') {
    // TypeScript يعرف أن response هنا من نوع Success
    console.log(`تم استرجاع ${response.data.length} عنصر`);
  } else {
    // TypeScript يعرف أن response هنا من نوع Error
    console.log(`حدث خطأ: ${response.message}`);
  }
}

تنظيم الكود باستخدام الوحدات

// models/user.ts
export interface User {
  id: number;
  name: string;
}

// services/api.ts
import { User } from '../models/user';

export async function getUsers(): Promise {
  // ...
}

// app.ts
import { getUsers } from './services/api';

async function initialize() {
  const users = await getUsers();
  // ...
}

استخدام enums بحكمة

// في بعض الحالات، يمكن استخدام union types بدلًا من enums
// بدلًا من
enum Color {
  Red,
  Green,
  Blue
}

// يمكن استخدام
type Color = 'red' | 'green' | 'blue';

// وهذا يمنع الأخطاء عند تعيين قيم غير متوقعة
function setColor(color: Color) {
  // ...
}

استخدام التحقق من null و undefined

// تأكد من فعّل خيار strictNullChecks في tsconfig.json
function processValue(value: string | null | undefined) {
  // يجب التحقق قبل استخدام value
  if (value) {
    console.log(value.toUpperCase());
  }
  
  // أو استخدام اختصار التهاون (nullish coalescing)
  const safeValue = value ?? "قيمة افتراضية";
  
  // أو استخدام اختصار الاستدعاء المتهاون (optional chaining)
  const length = value?.length;
}

كتابة توثيق JSDoc

/**
 * تقوم بحساب المجموع الكلي للمصفوفة
 * @param numbers مصفوفة الأرقام المراد جمعها
 * @returns مجموع كل الأرقام
 */
function sum(numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

تنظيم ملفات التعريف (Declaration Files)

// استخدام ملفات .d.ts لتعريف الأنواع
// types.d.ts
declare namespace App {
  interface User {
    id: number;
    name: string;
  }
  
  interface Config {
    apiUrl: string;
    timeout: number;
  }
}

استخدام أدوات التحليل (Linting)

استخدم ESLint مع قواعد TypeScript لضمان جودة الكود واتباع الممارسات المثلى:

// .eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ]
}