نظام الأنواع في TypeScript — من Generics إلى Conditional Types
دليل عميق في أنواع TypeScript المتقدمة: الـ Generics وتقييدها، والـ Conditional Types، والـ Mapped Types، مع أمثلة عملية من مشاريع حقيقية.
لماذا نتعمق في نظام الأنواع؟
كثير من المطورين يستخدمون TypeScript كـ "JavaScript بتعليقات"، يضيفون : string و : number هنا وهناك، ثم يلجؤون إلى any حين يصعب الأمر. هذا المقال يُغير هذا النمط من جذره.
نظام الأنواع في TypeScript يتيح لك التعبير عن قيود منطقية كاملة في وقت الكتابة — بدلًا من اكتشافها في وقت التشغيل.
Generics — الأنواع العامة
المفهوم الأساسي
الـ Generic هو متغير للنوع، تمامًا مثل متغير القيمة لكن على مستوى النوع.
// بدون Generics — تكرار ممل
function firstString(arr: string[]): string { return arr[0]; }
function firstNumber(arr: number[]): number { return arr[0]; }
// مع Generic — دالة واحدة لكل الأنواع
function first<T>(arr: T[]): T {
return arr[0];
}
const s = first(["a", "b", "c"]); // TypeScript يستنتج T = string
const n = first([1, 2, 3]); // TypeScript يستنتج T = numberتقييد الـ Generic بـ extends
// T يجب أن يكون له خاصية length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "world"); // OK
longest([1, 2], [3]); // OK
longest(10, 20); // خطأ! number ليس له lengthGeneric مع عدة معاملات
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((item, i) => [item, b[i]]);
}
const result = zip(["أحمد", "علي"], [25, 30]);
// النوع: [string, number][]Conditional Types — الأنواع الشرطية
الصيغة تشبه ternary العادي لكن على مستوى الأنواع:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // falseمثال عملي: استخراج نوع من Promise
type Awaited<T> = T extends Promise<infer R> ? R : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<string>; // string (لا promise)
type C = Awaited<Promise<Promise<number>>>; // Promise<number>الكلمة المفتاحية infer تقول: "إذا تطابق النمط، استخرج هذا الجزء كنوع جديد اسمه R".
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
// عند تمرير union type، ينتشر على كل عضو
type Arr = ToArray<string | number>;
// النتيجة: string[] | number[]
// وليس: (string | number)[]لمنع الانتشار التلقائي، ضع النوع بين قوسين مربعين: type ToArray<T> = [T] extends [any] ? T[] : never
Mapped Types — تحويل الأنواع
تسمح لك بإنشاء نوع جديد بتحويل خصائص نوع موجود:
// المدمجة في TypeScript
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };مثال متقدم: DeepPartial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type Config = {
server: { host: string; port: number };
database: { url: string; name: string };
};
// كل المستويات اختيارية الآن
const patch: DeepPartial<Config> = {
server: { port: 8080 }, // host غير مطلوب
};Template Literal Types
منذ TypeScript 4.1 يمكنك بناء أنواع نصية:
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSProperty =
| `margin-${"top" | "right" | "bottom" | "left"}`
| `padding-${"top" | "right" | "bottom" | "left"}`;جدول مقارنة الأدوات المدمجة
| الأداة | الوصف | مثال |
|---|---|---|
Partial<T> | كل الخصائص اختيارية | Partial<User> |
Required<T> | كل الخصائص إلزامية | Required<Config> |
Readonly<T> | كل الخصائص للقراءة فقط | Readonly<State> |
Pick<T, K> | اختيار خصائص محددة | Pick<User, "name" | "email"> |
Omit<T, K> | حذف خصائص محددة | Omit<User, "password"> |
Record<K, V> | خريطة مفاتيح وقيم | Record<string, number> |
Exclude<T, U> | حذف من Union | Exclude<"a" | "b", "a"> |
Extract<T, U> | استخراج من Union | Extract<"a" | "b", "a"> |
ReturnType<F> | نوع القيمة المُرجَعة | ReturnType<typeof fetch> |
Parameters<F> | أنواع المعاملات | Parameters<typeof fn> |
مثال نهائي: Type-safe Event Emitter
type EventMap = {
login: { userId: string; timestamp: number };
logout: { userId: string };
error: { message: string; code: number };
};
class TypedEmitter<Events extends Record<string, unknown>> {
private handlers = new Map<keyof Events, Set<Function>>();
on<K extends keyof Events>(
event: K,
handler: (data: Events[K]) => void
): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.handlers.get(event)?.forEach((fn) => fn(data));
}
}
const emitter = new TypedEmitter<EventMap>();
// TypeScript يعرف بدقة نوع data في كل حالة
emitter.on("login", (data) => console.log(data.userId));
emitter.on("error", (data) => console.log(data.code));
emitter.emit("login", { userId: "123", timestamp: Date.now() });كلما استثمرت في نظام الأنواع، كلما قل وقت الـ debugging وزادت ثقتك في كل refactor تجريه.