نظام الأنواع في TypeScript — من Generics إلى Conditional Types
TypeScript5 دقيقة للقراءة

نظام الأنواع في TypeScript — من Generics إلى Conditional Types

دليل عميق في أنواع TypeScript المتقدمة: الـ Generics وتقييدها، والـ Conditional Types، والـ Mapped Types، مع أمثلة عملية من مشاريع حقيقية.

لماذا نتعمق في نظام الأنواع؟

كثير من المطورين يستخدمون TypeScript كـ "JavaScript بتعليقات"، يضيفون : string و : number هنا وهناك، ثم يلجؤون إلى any حين يصعب الأمر. هذا المقال يُغير هذا النمط من جذره.

نظام الأنواع في TypeScript يتيح لك التعبير عن قيود منطقية كاملة في وقت الكتابة — بدلًا من اكتشافها في وقت التشغيل.


Generics — الأنواع العامة

المفهوم الأساسي

الـ Generic هو متغير للنوع، تمامًا مثل متغير القيمة لكن على مستوى النوع.

TypeScript
// بدون 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

TypeScript
// 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 ليس له length

Generic مع عدة معاملات

TypeScript
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 العادي لكن على مستوى الأنواع:

TypeScript
type IsString<T> = T extends string ? true : false;
 
type A = IsString<string>;  // true
type B = IsString<number>;  // false

مثال عملي: استخراج نوع من Promise

TypeScript
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

TypeScript
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
// المدمجة في 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

TypeScript
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 يمكنك بناء أنواع نصية:

TypeScript
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>حذف من UnionExclude<"a" | "b", "a">
Extract<T, U>استخراج من UnionExtract<"a" | "b", "a">
ReturnType<F>نوع القيمة المُرجَعةReturnType<typeof fetch>
Parameters<F>أنواع المعاملاتParameters<typeof fn>

مثال نهائي: Type-safe Event Emitter

TypeScript
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 تجريه.