لغة TypeScript

الأصناف في TypeScript

28 دقيقة الدرس 13 من 40

البرمجة الكائنية مع أصناف TypeScript

TypeScript يعزز أصناف JavaScript بميزات أنواع قوية، ومعدلات الوصول، والأصناف المجردة، والمزيد. فهم أصناف TypeScript ضروري لبناء تطبيقات كائنية قوية.

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

أصناف TypeScript تمتد بناء جملة صنف JavaScript ES6 بتعليقات توضيحية للأنواع وميزات إضافية:

// تعريف صنف أساسي class Person { // خصائص مع تعليقات توضيحية للأنواع name: string; age: number; // المُنشئ constructor(name: string, age: number) { this.name = name; this.age = age; } // طريقة greet(): string { return `Hello, I'm ${this.name} and I'm ${this.age} years old.`; } // طريقة مع معاملات haveBirthday(): void { this.age++; console.log(`Happy birthday! Now I'm ${this.age}.`); } } // إنشاء مثيلات const person1 = new Person("Alice", 30); const person2 = new Person("Bob", 25); console.log(person1.greet()); // "Hello, I'm Alice and I'm 30 years old." person2.haveBirthday(); // "Happy birthday! Now I'm 26."
ملاحظة: في TypeScript، يجب عليك التصريح صراحةً بخصائص الصنف قبل استخدامها في المُنشئ. هذا يختلف عن JavaScript حيث يمكن إضافة الخصائص ديناميكياً.

معدلات الوصول (Access Modifiers)

TypeScript يوفر ثلاثة معدلات وصول للتحكم في رؤية الخصائص والطرق:

class BankAccount { // عام: يمكن الوصول إليه من أي مكان (افتراضي) public accountHolder: string; // خاص: يمكن الوصول إليه فقط داخل الصنف private balance: number; // محمي: يمكن الوصول إليه داخل الصنف والأصناف الفرعية protected accountNumber: string; constructor(holder: string, initialBalance: number) { this.accountHolder = holder; this.balance = initialBalance; this.accountNumber = this.generateAccountNumber(); } // طريقة عامة public deposit(amount: number): void { if (amount > 0) { this.balance += amount; console.log(`Deposited $${amount}. New balance: $${this.balance}`); } } // طريقة عامة public getBalance(): number { return this.balance; } // طريقة خاصة private generateAccountNumber(): string { return `ACC-${Math.random().toString(36).substr(2, 9).toUpperCase()}`; } // طريقة محمية protected logTransaction(type: string, amount: number): void { console.log(`[${type}] $${amount} - Account: ${this.accountNumber}`); } } const account = new BankAccount("Alice", 1000); account.deposit(500); // موافق: طريقة عامة console.log(account.accountHolder); // موافق: خاصية عامة console.log(account.getBalance()); // موافق: طريقة عامة // account.balance; // خطأ: خاصية خاصة // account.accountNumber; // خطأ: خاصية محمية // account.generateAccountNumber(); // خطأ: طريقة خاصة
نصيحة: استخدم private لتفاصيل التنفيذ التي لا يجب الوصول إليها خارج الصنف، protected للأعضاء التي تحتاج الأصناف الفرعية للوصول إليها، و public (أو احذف المعدّل) للواجهة البرمجية العامة.

خصائص المعاملات (Parameter Properties)

TypeScript يوفر بناء جملة مختصر للتصريح عن الخصائص وتهيئتها في المُنشئ:

// الطريقة التقليدية class User1 { name: string; email: string; age: number; constructor(name: string, email: string, age: number) { this.name = name; this.email = email; this.age = age; } } // اختصار خصائص المعاملات class User2 { constructor( public name: string, public email: string, private age: number ) { // الخصائص يتم التصريح عنها وتهيئتها تلقائياً } getAge(): number { return this.age; } } const user = new User2("Alice", "alice@example.com", 30); console.log(user.name); // "Alice" console.log(user.email); // "alice@example.com" // console.log(user.age); // خطأ: خاصية خاصة console.log(user.getAge()); // 30 // دمج خصائص المعاملات مع التهيئة العادية class Product { private createdAt: Date; constructor( public id: number, public name: string, private price: number ) { this.createdAt = new Date(); } getPrice(): number { return this.price; } getCreatedAt(): Date { return this.createdAt; } }

الخصائص للقراءة فقط (Readonly Properties)

استخدم معدّل readonly لإنشاء خصائص يمكن تعيينها فقط أثناء التهيئة:

class Circle { readonly PI: number = 3.14159; readonly radius: number; constructor(radius: number) { this.radius = radius; } getArea(): number { return this.PI * this.radius ** 2; } // هذا سيسبب خطأ: // setRadius(newRadius: number): void { // this.radius = newRadius; // خطأ: خاصية للقراءة فقط // } } const circle = new Circle(5); console.log(circle.getArea()); // 78.53975 // circle.radius = 10; // خطأ: خاصية للقراءة فقط // circle.PI = 3.14; // خطأ: خاصية للقراءة فقط // دمج readonly مع خصائص المعاملات class ImmutablePoint { constructor( public readonly x: number, public readonly y: number ) {} distanceFromOrigin(): number { return Math.sqrt(this.x ** 2 + this.y ** 2); } } const point = new ImmutablePoint(3, 4); console.log(point.x); // 3 console.log(point.distanceFromOrigin()); // 5 // point.x = 10; // خطأ: خاصية للقراءة فقط

Getters و Setters

TypeScript يدعم طرق الوصول للتحكم في الوصول إلى الخصائص:

class Temperature { private _celsius: number = 0; get celsius(): number { return this._celsius; } set celsius(value: number) { if (value < -273.15) { throw new Error("Temperature cannot be below absolute zero"); } this._celsius = value; } get fahrenheit(): number { return (this._celsius * 9/5) + 32; } set fahrenheit(value: number) { this.celsius = (value - 32) * 5/9; } } const temp = new Temperature(); temp.celsius = 25; // يستخدم setter console.log(temp.celsius); // 25 (يستخدم getter) console.log(temp.fahrenheit); // 77 (يستخدم getter) temp.fahrenheit = 98.6; // يستخدم setter console.log(temp.celsius); // 37 (تقريباً) // temp.celsius = -300; // خطأ: لا يمكن أن تكون درجة الحرارة تحت الصفر المطلق // خاصية للقراءة فقط باستخدام getter فقط class Counter { private _count: number = 0; get count(): number { return this._count; } increment(): void { this._count++; } decrement(): void { this._count--; } } const counter = new Counter(); counter.increment(); console.log(counter.count); // 1 // counter.count = 10; // خطأ: لا يمكن التعيين (لا يوجد setter)
تحذير: getters في TypeScript يجب ألا تحتوي على معاملات، و setters يجب أن تحتوي على معامل واحد بالضبط. نوع الإرجاع لـ getter لا يمكن أن يكون void، ونوع الإرجاع لـ setter دائماً void.

الوراثة (Inheritance)

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

// الصنف الأساسي class Animal { constructor( public name: string, protected age: number ) {} makeSound(): void { console.log("Some generic animal sound"); } getInfo(): string { return `${this.name} is ${this.age} years old`; } } // الصنف المشتق class Dog extends Animal { constructor( name: string, age: number, public breed: string ) { super(name, age); // استدعاء مُنشئ الوالد } // تجاوز طريقة الوالد makeSound(): void { console.log("Woof! Woof!"); } // طريقة إضافية fetch(): void { console.log(`${this.name} is fetching the ball!`); } // تجاوز مع منطق إضافي getInfo(): string { return `${super.getInfo()} and is a ${this.breed}`; } } const dog = new Dog("Buddy", 3, "Golden Retriever"); dog.makeSound(); // "Woof! Woof!" dog.fetch(); // "Buddy is fetching the ball!" console.log(dog.getInfo()); // "Buddy is 3 years old and is a Golden Retriever" // يمكن استخدامه كنوع أساسي const animal: Animal = dog; animal.makeSound(); // لا يزال يستدعي تنفيذ Dog: "Woof! Woof!"

الأصناف المجردة (Abstract Classes)

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

// صنف أساسي مجرد abstract class Shape { constructor(public color: string) {} // طريقة مجردة - يجب تنفيذها بواسطة الأصناف المشتقة abstract getArea(): number; // طريقة مجردة مع توقيع مختلف في الأصناف المشتقة abstract getPerimeter(): number; // طريقة ملموسة - موروثة بواسطة الأصناف المشتقة describe(): string { return `A ${this.color} shape with area ${this.getArea()}`; } } // لا يمكن إنشاء مثيل من صنف مجرد // const shape = new Shape("red"); // خطأ class Rectangle extends Shape { constructor( color: string, public width: number, public height: number ) { super(color); } getArea(): number { return this.width * this.height; } getPerimeter(): number { return 2 * (this.width + this.height); } } class Circle extends Shape { constructor( color: string, public radius: number ) { super(color); } getArea(): number { return Math.PI * this.radius ** 2; } getPerimeter(): number { return 2 * Math.PI * this.radius; } } const rect = new Rectangle("blue", 10, 5); console.log(rect.getArea()); // 50 console.log(rect.describe()); // "A blue shape with area 50" const circle = new Circle("red", 7); console.log(circle.getArea()); // 153.93804002589985 console.log(circle.getPerimeter()); // 43.982297150257104 // الأصناف المجردة تمكّن من تعدد الأشكال const shapes: Shape[] = [rect, circle]; shapes.forEach(shape => { console.log(shape.describe()); });
ملاحظة: الأصناف المجردة يمكن أن تحتوي على مُنشئات وطرق ملموسة وخصائص. إنها مفيدة لمشاركة الوظائف الشائعة مع فرض تنفيذ طرق معينة بواسطة الأصناف المشتقة.

تنفيذ الواجهات (Implementing Interfaces)

يمكن للأصناف تنفيذ واجهات لضمان توفير وظائف محددة:

// تعريف الواجهة interface Printable { print(): void; } interface Serializable { serialize(): string; deserialize(data: string): void; } // صنف ينفذ واجهة واحدة class Document implements Printable { constructor(public content: string) {} print(): void { console.log(this.content); } } // صنف ينفذ واجهات متعددة class Report implements Printable, Serializable { constructor( public title: string, public content: string ) {} print(): void { console.log(`=== ${this.title} ===`); console.log(this.content); } serialize(): string { return JSON.stringify({ title: this.title, content: this.content }); } deserialize(data: string): void { const obj = JSON.parse(data); this.title = obj.title; this.content = obj.content; } } const report = new Report("Q4 Report", "Sales increased by 20%"); report.print(); // === Q4 Report === // Sales increased by 20% const serialized = report.serialize(); console.log(serialized); // {"title":"Q4 Report","content":"Sales increased by 20%"} const newReport = new Report("", ""); newReport.deserialize(serialized); newReport.print(); // === Q4 Report === // Sales increased by 20%

الأعضاء الثابتة (Static Members)

الخصائص والطرق الثابتة تنتمي إلى الصنف نفسه، وليس إلى المثيلات:

class MathUtils { // خاصية ثابتة static PI: number = 3.14159; // طريقة ثابتة static calculateCircleArea(radius: number): number { return this.PI * radius ** 2; } // طريقة ثابتة تصل إلى خاصية ثابتة static degreesToRadians(degrees: number): number { return (degrees * this.PI) / 180; } // طريقة المثيل يمكنها الوصول إلى الأعضاء الثابتة عبر اسم الصنف static radiansToDegrees(radians: number): number { return (radians * 180) / MathUtils.PI; } } // الوصول إلى الأعضاء الثابتة عبر اسم الصنف console.log(MathUtils.PI); // 3.14159 console.log(MathUtils.calculateCircleArea(5)); // 78.53975 console.log(MathUtils.degreesToRadians(180)); // 3.14159 // الأعضاء الثابتة غير متاحة على المثيلات const utils = new MathUtils(); // console.log(utils.PI); // خطأ: عضو ثابت // مثال عملي: نمط Factory class User { private static nextId: number = 1; constructor( public id: number, public name: string, public email: string ) {} static createUser(name: string, email: string): User { const user = new User(this.nextId++, name, email); return user; } } const user1 = User.createUser("Alice", "alice@example.com"); const user2 = User.createUser("Bob", "bob@example.com"); console.log(user1.id); // 1 console.log(user2.id); // 2
تمرين: أنشئ صنف مجرد Vehicle بخصائص brand و model، وطرق مجردة start() و stop(). نفذ صنفين ملموسين: Car بخاصية numberOfDoors و Motorcycle بخاصية hasSidecar. أضف طريقة getDescription() إلى الصنف الأساسي تستخدم الطرق المجردة. أنشئ مثيلات واختبر تعدد الأشكال بتخزينها في مصفوفة Vehicle[].

تعبيرات الصنف (Class Expressions)

مثل الدوال، يمكن تعريف الأصناف كتعبيرات:

// تعبير صنف مُسمى const PersonClass = class Person { constructor(public name: string) {} greet(): string { return `Hello, I'm ${this.name}`; } }; const person = new PersonClass("Alice"); console.log(person.greet()); // "Hello, I'm Alice" // تعبير صنف مجهول const GreeterClass = class { greet(name: string): string { return `Hello, ${name}!`; } }; const greeter = new GreeterClass(); console.log(greeter.greet("Bob")); // "Hello, Bob!" // تعبير صنف كقيمة إرجاع دالة function createGreeterClass(greeting: string) { return class { greet(name: string): string { return `${greeting}, ${name}!`; } }; } const SpanishGreeter = createGreeterClass("Hola"); const germanGreeter = new (createGreeterClass("Guten Tag"))(); const spanish = new SpanishGreeter(); console.log(spanish.greet("Carlos")); // "Hola, Carlos!" console.log(germanGreeter.greet("Hans")); // "Guten Tag, Hans!"

الخلاصة

أصناف TypeScript توفر أساساً قوياً للبرمجة الكائنية مع ميزات مثل معدلات الوصول، وخصائص المعاملات، و getters/setters، والوراثة، والأصناف المجردة، وتنفيذ الواجهات. هذه الميزات تمكنك من إنشاء كود كائني جيد التنظيم وقابل للصيانة وآمن للأنواع. فهم الأصناف ضروري لبناء تطبيقات TypeScript المعقدة والاستفادة من أنماط التصميم بفعالية.