الوحدة 3 · البرمجة على مستوى الأنواع
الـ Conditional Types والتوزيع و infer
22 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
اكتب conditional types تختار بين الأنواع، وتوقّع كيف تتوزّع على الـ unions، واستخرج أنواعًا من أنواع أخرى بـ infer، معيدًا بناء Exclude وExtract وReturnType.
- 1العامل الثلاثي على مستوى الأنواع
- 2التوزيع على الـ unions
- 3infer: التقط جزءًا من نوع
- 4تصفية المفاتيح حسب نوع القيمة
- 5أنواع الإرجاع الشرطية: تعامل معها بحذر
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
في تصميم عميل API بأنواع دقيقة، أجبرت المسارات التي لا مدخل لها المستدعين على كتابة call('GET /me', {}). الإصلاح يحتاج إلى نوع يتّخذ قرارًا: “إن لم يكن في مدخل هذا المسار حقول إلزامية، فاجعل الوسيط اختياريًا؛ وإلا فاطلبه”. الـ mapped types تمرّ على المفاتيح. والـ conditional types (الأنواع الشرطية) تقرّر.
العامل الثلاثي على مستوى الأنواع
type IsArray<T> = T extends readonly unknown[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<Order>; // falseT extends U ? X : Y تُقرأ مثل العامل الثلاثي: إن كان T قابلًا للإسناد إلى U فالنتيجة X، وإلا Y. وextends هنا هي فحص قابلية الإسناد نفسه الموجود في كل مكان آخر في TypeScript: “هل T مجموعة جزئية من U؟”. وحين يكون T معاملًا generic غير معروف بعد، يؤجّل TypeScript الشرط ويُبقيه كما هو حتى يصل نوع محدّد.
إليك إصلاح عميل الـ API. {} extends Input صادقة تمامًا حين تكون كل خصائص Input اختيارية (فالكائن الفارغ سيحقّقها):
type CallArgs<R extends keyof Routes> = {} extends Routes[R]['input']
? [input?: Routes[R]['input']]
: [input: Routes[R]['input']];
declare function call<R extends keyof Routes>(route: R, ...args: CallArgs<R>): Promise<Routes[R]['output']>;
call('GET /me'); // ok: input is {}
call('GET /orders'); // ok: every filter is optional
call('GET /orders/:id'); // Error: Expected 2 arguments, but got 1.
call('GET /orders/:id', { id: 1 }); // okينتج الشرط نوع tuple، وينشره معامل البقية (rest parameter) ...args في قائمة المعاملات. وعناصر الـ tuple الموسومة (input?:) تُبقي اسم المعامل في التلميحات. ولم يتغيّر موضع استدعاء واحد.
التوزيع على الـ unions
للـ conditional types سلوك واحد يفاجئ الجميع. حين يكون النوع المفحوص معامل نوع مجرّدًا (naked type parameter) وتمرّر إليه union، يُنفَّذ الشرط مرة لكل عضو وتُجمع النتائج:
type ToArray<T> = T extends unknown ? T[] : never;
type X = ToArray<string | number>; // string[] | number[], not (string | number)[]يُسمّى هذا التوزيع (distribution)، وهو المحرّك وراء عدد من الـ utility types. إليك Exclude وExtract، كما تعرّفهما الـ lib بالضبط:
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type Active = MyExclude<OrderStatus, 'cancelled' | 'returned'>; // 'processing' | 'shipped' | 'delivered'
type Shipped = MyExtract<Order, { status: 'shipped' }>; // the ShippedOrder variantكل عضو يمرّ عبر الشرط وحده. والأعضاء المحوَّلة إلى never تختفي، لأن never هو الـ union الفارغ. وMyExtract<Order, { status: 'shipped' }> هي أنظف طريقة لتسمية شكل واحد من discriminated union.
لإيقاف التوزيع، غلّف الطرفين في tuple من عنصر واحد. [T] ليس معامل نوع مجرّدًا، فيُفحص الـ union ككل:
type ToArrayWhole<T> = [T] extends [unknown] ? T[] : never;
type Y = ToArrayWhole<string | number>; // (string | number)[]infer: التقط جزءًا من نوع
داخل جملة extends، تعلن infer X متغيّر نوع يلتقط ما يقع في ذلك الموضع، إن طابق النمط. وهكذا تعرّف الـ lib النوع ReturnType في جوهره (نسخة الـ lib تقيّد F أيضًا بنوع دالة وترجع إلى any):
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type Loaded = MyReturnType<typeof loadOrder>; // Promise<Order>الحيلة نفسها تستخرج أنواعًا من أي شيء له بنية:
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type FirstArg<F> = F extends (first: infer A, ...rest: any[]) => any ? A : never;ويمكن أن تحمل infer قيدًا أيضًا، كما في infer S extends string، فلا تطابق إلا حين يكون النوع الملتقَط نصًا، وتعطيك S مضيَّقًا مسبقًا. ستحتاج إلى ذلك في الدرس القادم، حين تلتقط أجزاءً من الـ literal types النصية.
تصفية المفاتيح حسب نوع القيمة
اجمع mapped type وشرطًا ووصولًا بالفهرس، وتستطيع اختيار المفاتيح حسب ماهية قيمها:
type KeysOfType<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
type Line = { sku: string; quantity: number; unitPriceCents: number };
type NumericField = KeysOfType<Line, number>; // 'quantity' | 'unitPriceCents'يحوّل الـ mapped type كل خاصية إلى اسم مفتاحها أو إلى never، ثم تجمع [keyof T] القيم في union، تختفي فيه قيم never. يُقرأ بالمقلوب في المرة الأولى؛ اكتبه مرة، وسمِّه جيدًا، وأعِد استخدامه. والآن دالة ترتيب نوعها sortBy(lines, key: KeysOfType<Line, number>) ترفض الترتيب حسب sku.
أنواع الإرجاع الشرطية: تعامل معها بحذر
من المغري إعطاء دالة نوع إرجاع شرطيًا، مثل function format<T extends number | number[]>(x: T): T extends number ? string : string[]. يحصل المستدعون على أنواع دقيقة، لكن التنفيذ لا يحصل عليها: داخل الدالة، يبقى T مجهولًا، ويبقى الشرط مؤجّلًا، ولا يستطيع TypeScript أن يتحقّق من أن return x.toFixed(2) تطابقه. فينتهي بك الأمر إلى كتابة as any أو as T extends number ? string : string[] على كل جملة عودة، أي أن أدقّ كود في الدالة هو الكود الذي لا يفحصه أحد.
لهذا فضّل القسم السابق الـ overloads أو الدوال المنفصلة لهذه المهمة. تتألّق الـ conditional types في الأنواع المشتقّة (CallArgs وExtract وKeysOfType) حيث لا يلزم تنفيذ شيء مقابلها، وتكون في أضعف حالاتها كنوع إرجاع معلَن لمنطق مكتوب يدويًا.
في التمرين ستعيد بناء Exclude وExtract وReturnType وتستخدمها على أنواع الطلبات في Cartwheel. والدرس القادم يطبّق الشروط وinfer على النصوص.
أهم النقاط
T extends U ? X : YتختارXحين يكونTقابلًا للإسناد إلىU، وYفي غير ذلك؛ ومعTمن نوع generic، ينتظر الاختيار حتى يُعرفT.- الشرط على معامل نوع مجرّد يتوزّع على الـ unions، فيُنفَّذ مرة لكل عضو ثم تُجمع النتائج.
- غلّف الطرفين بأقواس مربّعة،
[T] extends [U]، لتختبر الـ union ككل بدل التوزيع. inferتعلن متغيّر نوع داخل جملةextendsوتلتقط ما يطابقه، وهكذا يعملReturnTypeوAwaited.- تحويل كل مفتاح إلى نفسه أو إلى
neverثم الفهرسة بـ[keyof T]يصفّي المفاتيح حسب نوع قيمتها.