مقدمة إلى الأنواع العامة (Generics)
الأنواع العامة (Generics) هي إحدى أقوى ميزات TypeScript، حيث تسمح لك بإنشاء مكونات قابلة لإعادة الاستخدام تعمل مع أنواع متعددة مع الحفاظ على سلامة الأنواع. بدلاً من العمل مع أنواع محددة، تتيح لك الأنواع العامة كتابة كود يمكنه التكيف مع أنواع مختلفة يحددها المستدعي.
لماذا تهم الأنواع العامة
بدون الأنواع العامة، ستحتاج إلى استخدام النوع any (مما يفقدك سلامة الأنواع) أو إنشاء كود مكرر لكل نوع. تحل الأنواع العامة كلتا المشكلتين من خلال توفير:
- سلامة الأنواع: فحص الأنواع في وقت الترجمة مع دعم IntelliSense الكامل
- قابلية إعادة استخدام الكود: اكتب مرة واحدة واستخدم مع أي نوع
- المرونة: المستدعي يقرر النوع الملموس عند الاستخدام
- التوثيق: قيود الأنواع العامة تجعل متطلبات الأنواع صريحة
الدوال العامة (Generic Functions)
أبسط شكل من الأنواع العامة هو الدالة العامة. لنبدأ بمثال أساسي:
// بدون الأنواع العامة - فقدان معلومات النوع
function identityAny(arg: any): any {
return arg;
}
const result1 = identityAny(42); // النوع: any (لا توجد سلامة أنواع)
const result2 = identityAny("hello"); // النوع: any
// مع الأنواع العامة - الحفاظ على معلومات النوع
function identity<T>(arg: T): T {
return arg;
}
const result3 = identity<number>(42); // النوع: number
const result4 = identity<string>("hello"); // النوع: string
// استنتاج النوع - TypeScript يستنتج النوع
const result5 = identity(42); // النوع: number (مستنتج)
const result6 = identity("hello"); // النوع: string (مستنتج)
ملاحظة: بناء الجملة <T> يقدم معامل نوع. T هو اصطلاح (يرمز إلى "Type")، لكن يمكنك استخدام أي معرف صالح. معامل النوع يعمل كعنصر نائب يتم استبداله بنوع فعلي عند استدعاء الدالة.
الدوال العامة مع المصفوفات
الأنواع العامة مفيدة بشكل خاص عند العمل مع المجموعات:
// دالة عامة تعمل مع المصفوفات
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const firstNumber = getFirstElement([1, 2, 3]); // النوع: number | undefined
const firstString = getFirstElement(["a", "b"]); // النوع: string | undefined
// دالة عامة مع معاملات متعددة
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: "Alice" }, { age: 30 });
// النوع: { name: string } & { age: number }
console.log(merged.name); // "Alice"
console.log(merged.age); // 30
// دالة عامة مع عمليات المصفوفات
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const numbers = [1, 2, 3];
const strings = map(numbers, (n) => n.toString()); // النوع: string[]
const doubled = map(numbers, (n) => n * 2); // النوع: number[]
الواجهات العامة (Generic Interfaces)
يمكنك تعريف واجهات عامة لإنشاء عقود أنواع قابلة لإعادة الاستخدام:
// واجهة عامة لزوج مفتاح-قيمة
interface Pair<K, V> {
key: K;
value: V;
}
const stringNumberPair: Pair<string, number> = {
key: "age",
value: 30
};
const numberBooleanPair: Pair<number, boolean> = {
key: 1,
value: true
};
// واجهة عامة لاستجابات API
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: number;
name: string;
email: string;
}
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Alice", email: "alice@example.com" },
status: 200,
message: "Success"
};
const usersResponse: ApiResponse<User[]> = {
data: [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
],
status: 200,
message: "Success"
};
الأصناف العامة (Generic Classes)
يمكن للأصناف أيضاً استخدام الأنواع العامة لإنشاء هياكل بيانات قابلة لإعادة الاستخدام:
// هيكل بيانات Stack عام
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// الاستخدام مع الأرقام
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
console.log(numberStack.peek()); // 1
// الاستخدام مع النصوص
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.pop()); // "world"
// صنف عام مع معاملات نوع متعددة
class KeyValueStore<K, V> {
private store = new Map<K, V>();
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
has(key: K): boolean {
return this.store.has(key);
}
delete(key: K): boolean {
return this.store.delete(key);
}
}
const cache = new KeyValueStore<string, User>();
cache.set("user1", { id: 1, name: "Alice", email: "alice@example.com" });
const user = cache.get("user1"); // النوع: User | undefined
قيود الأنواع العامة (Generic Constraints)
أحياناً تحتاج إلى تقييد الأنواع التي يمكن استخدامها مع النوع العام. القيود تتيح لك تحديد المتطلبات:
// قيد: T يجب أن يحتوي على خاصية length
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // موافق: string لديه length
logLength([1, 2, 3]); // موافق: array لديه length
logLength({ length: 10, value: "test" }); // موافق: object لديه length
// logLength(42); // خطأ: number ليس لديه length
// قيد: T يجب أن يحتوي على خصائص محددة
interface Identifiable {
id: number;
}
function findById<T extends Identifiable>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
interface Product extends Identifiable {
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 25 }
];
const product = findById(products, 1); // النوع: Product | undefined
// قيد: T يجب أن يمتد من معامل نوع آخر
function copyProperties<T extends U, U>(target: T, source: U): T {
return Object.assign(target, source);
}
const result = copyProperties(
{ name: "Alice", age: 30 },
{ name: "Bob" }
);
// result.name هو "Bob"، result.age هو 30
نصيحة: استخدم قيود الأنواع العامة لجعل كودك أكثر قوة. فهي توفر ضمانات وقت الترجمة حول العمليات التي يمكنك إجراؤها على النوع العام، مما يمنع أخطاء وقت التشغيل.
استخدام keyof مع الأنواع العامة
عامل keyof مع الأنواع العامة ينشئ أنماط قوية وآمنة للأنواع:
// وصول آمن للأنواع إلى الخصائص
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 setProperty<T, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): void {
obj[key] = value;
}
setProperty(person, "age", 31); // موافق
// setProperty(person, "age", "31"); // خطأ: نوع خاطئ
// setProperty(person, "invalid", 123); // خطأ: مفتاح غير صالح
// الحصول على جميع قيم كائن بشكل آمن للأنواع
function getValues<T>(obj: T): T[keyof T][] {
return Object.keys(obj).map(key => obj[key as keyof T]);
}
const values = getValues(person); // النوع: (string | number)[]
الأنواع العامة الافتراضية (Default Generic Types)
يمكنك تحديد أنواع افتراضية لمعاملات الأنواع العامة:
// نوع عام مع نوع افتراضي
interface Container<T = string> {
value: T;
}
const stringContainer: Container = { value: "hello" }; // يستخدم الافتراضي (string)
const numberContainer: Container<number> = { value: 42 };
// دالة عامة مع نوع افتراضي
function createArray<T = number>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const numbers = createArray(3, 0); // النوع: number[]
const strings = createArray<string>(3, ""); // النوع: string[]
const defaultArray = createArray(3, 42); // النوع: number[] (افتراضي)
// أنواع عامة متعددة مع قيم افتراضية
interface ApiConfig<T = any, E = Error> {
data?: T;
error?: E;
loading: boolean;
}
const config1: ApiConfig = { loading: false }; // يستخدم الافتراضيات
const config2: ApiConfig<User> = {
data: { id: 1, name: "Alice", email: "alice@example.com" },
loading: false
};
const config3: ApiConfig<User, CustomError> = {
error: new CustomError("Failed"),
loading: false
};
class CustomError extends Error {
code: number;
constructor(message: string) {
super(message);
this.code = 500;
}
}
تحذير: يجب أن تأتي الأنواع العامة الافتراضية بعد معاملات الأنواع المطلوبة. لا يمكنك الحصول على معامل نوع مطلوب بعد معامل اختياري. ضع دائماً الافتراضيات في نهاية قائمة معاملات الأنواع العامة.
الأسماء المستعارة للأنواع العامة (Generic Type Aliases)
يمكن أن تكون الأسماء المستعارة للأنواع أيضاً عامة، لإنشاء أنواع معقدة قابلة لإعادة الاستخدام:
// اسم مستعار لنوع عام للأنواع القابلة للإلغاء
type Nullable<T> = T | null;
let name: Nullable<string> = "Alice";
name = null; // موافق
// اسم مستعار لنوع عام للدوال غير المتزامنة
type AsyncFunction<T> = () => Promise<T>
const fetchUser: AsyncFunction<User> = async () => {
const response = await fetch("/api/user");
return response.json();
};
// اسم مستعار لنوع عام لمصفوفة أو قيمة واحدة
type OneOrMany<T> = T | T[];
function process(value: OneOrMany<number>): void {
const values = Array.isArray(value) ? value : [value];
values.forEach(v => console.log(v));
}
process(42); // موافق
process([1, 2, 3]); // موافق
// اسم مستعار لنوع عام مع قيود
type Dictionary<T extends string | number | symbol = string> = {
[key in T]: any;
};
const stringDict: Dictionary = { a: 1, b: 2 };
const numberDict: Dictionary<number> = { 1: "a", 2: "b" };
تمرين: أنشئ نوع عام Result<T, E> يمثل إما نجاحاً مع بيانات من النوع T أو خطأ من النوع E. نفذ دوال مساعدة ok<T>(data: T) و err<E>(error: E) لإنشاء مثيلات Result. ثم أنشئ دالة تستخدم هذا النمط لتحليل JSON بشكل آمن.
أفضل الممارسات للأنواع العامة
- استخدم أسماء ذات معنى: للحالات البسيطة،
T جيد. للحالات المعقدة، استخدم أسماء وصفية مثل TData, TError, TKey
- حافظ على البساطة: لا تبالغ في الهندسة بعدد كبير جداً من معاملات الأنواع العامة
- أضف قيود: استخدم
extends لتوثيق متطلبات الأنواع
- استفد من الاستنتاج: دع TypeScript يستنتج الأنواع عندما يكون ذلك ممكناً لتقليل التعقيد
- استخدم الافتراضيات بحكمة: وفر افتراضيات معقولة لحالات الاستخدام الشائعة
- وثق الأنواع العامة: أضف تعليقات JSDoc تشرح معاملات الأنواع والقيود
الخلاصة
الأنواع العامة هي حجر الزاوية في نظام أنواع TypeScript، مما يمكنك من كتابة كود مرن وقابل لإعادة الاستخدام وآمن للأنواع. لقد تعلمت كيفية إنشاء دوال وواجهات وأصناف وأسماء مستعارة للأنواع عامة، وتطبيق قيود لتقييد الأنواع العامة، واستخدام أنواع افتراضية للسيناريوهات الشائعة. فهم الأنواع العامة ضروري للعمل مع قواعد كود ومكتبات TypeScript الحديثة.