النوع الشرطي يختار بين نوعين باختبار يشبه العامل الثلاثي في JavaScript: T extends U ? X : Y. إذا كان T قابلًا للإسناد إلى U فالنتيجة X، وإلا فهي Y.
تعني extends هنا «قابل للإسناد إلى»، وهي العلاقة نفسها التي يستخدمها المترجم عندما تسند قيمة إلى متغير. الأنواع الشرطية موجودة وقت الترجمة فقط، وتُحذف من مخرجات JavaScript.
الصياغة
type Result = CheckedType extends TestType ? TrueType : FalseType;
تصبح الأنواع الشرطية مفيدة مع generics، حيث يكون النوع المفحوص معامل نوع يأخذ نوعًا محددًا لاحقًا. في فرع true تعرف TypeScript أن النوع المفحوص يطابق الاختبار، لذا يُسمح بـ T["message"] في المثال السابق مع أن T المجرد لا يملك الخاصية message.
ويمكن تداخلها أيضًا، مثل العوامل الثلاثية المتسلسلة:
الجزء as TypeName<T> في جسم الدالة ليس اختياريًا، كما يبيّن القسم التالي.
أنواع القيمة المُعادة الشرطية تحتاج إلى تأكيد نوع
الدالة التي نوع قيمتها المُعادة نوع شرطي على معامل نوعها لا تستطيع أن تعيد أيًّا من الفرعين مباشرة. لا تضيّق TypeScript النوع T داخل الجسم، فلا تستطيع معرفة أي فرع ينطبق:
index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.
هناك حلان شائعان: التحميل الزائد (overloads)، الذي يصرّح بكل زوج من المدخلات والمخرجات ويفحص المستدعين بدقة، أو تأكيد نوع داخل التنفيذ.
يشرح درس التحميل الزائد للدوال الـ overloads بالتفصيل. أما مع تأكيد النوع فالمترجم يثق بك: لن يُكتشف فرع خاطئ في الجسم.
الأنواع الشرطية التوزيعية
عندما يكون النوع المفحوص معامل نوع مجردًا ويتلقى union، يعمل الشرط مرة لكل عضو وتُجمع النتائج في union جديد:
التوزيع هو ما يجعل Exclude وExtract تعملان. تُعرَّف Exclude<T, U> بأنها T extends U ? never : T: كل عضو يطابق U يصبح never، وnever يختفي من أي union. لذلك فإن Exclude<"a" | "b" | "c", "a"> هو "b" | "c".
وتنتج مفاجأتان عن القاعدة نفسها. النوع boolean هو union من true | false، لذا فإن ToArray<boolean> هو false[] | true[] وليس boolean[]. وnever هو union فارغ، فالنوع الشرطي التوزيعي الذي يتلقى never يعيد never دون أن يختبر شيئًا:
type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true
استخراج الأنواع بـ infer
تعلن infer متغير نوع جديدًا داخل جملة extends. إذا نجحت المطابقة تملأ TypeScript هذا المتغير من النوع المفحوص، ويمكنك استخدامه في فرع true:
اقرأ T extends Promise<infer V> ? V : T هكذا: «إذا كان T وعدًا بشيء ما، فسمِّ هذا الشيء V وأعده؛ وإلا فأعد T كما هو». لا يُسمح بـ infer إلا في جملة extends من نوع شرطي.
يمكن أن يحمل متغير infer قيدًا خاصًا به عبر extends. عندها لا تنجح المطابقة إلا إذا كان النوع المستنتج مناسبًا:
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string
بناء ReturnType بنفسك
ReturnType المدمج نوع شرطي من سطر واحد مع infer. كتابته بنفسك هي التمرين الكلاسيكي الذي يوضّح الفكرتين معًا:
يحوّل typeof makeUser قيمة الدالة إلى نوعها، ثم يطابقه النوع الشرطي مع «أي دالة» ويلتقط نوع القيمة المُعادة باسم R. تختلف نسخة المكتبة القياسية في أمرين: معاملها مقيّد بأنواع الدوال (T extends (...args: any) => any)، فيكون ReturnType<string> خطأ ترجمة بدلًا من never، وفرعها الخاطئ هو any. تشرح صفحة ReturnType أيضًا Parameters وInstanceType وAwaited.
الأنواع الشرطية العودية
يمكن للنوع الشرطي أن يشير إلى نفسه، وهذا يسمح له بفك أي عمق من التداخل:
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
يعمل Awaited<T> المدمج بهذه الطريقة، فيفك Promise<Promise<T>> حتى يصل إلى T. أبقِ العودية ضحلة في الممارسة: العودية العميقة جدًا أو غير المحدودة تجعل المترجم يتوقف بالخطأ error TS2589: Type instantiation is excessively deep and possibly infinite.
مرجع سريع
| النمط | المعنى |
|---|---|
T extends U ? X : Y | X إذا كان T قابلًا للإسناد إلى U، وإلا Y |
T extends U ? never : T | حذف الأعضاء المطابقة لـ U (هذا هو Exclude) |
T extends U ? T : never | إبقاء الأعضاء المطابقة لـ U (هذا هو Extract) |
[T] extends [U] ? X : Y | الاختبار نفسه، دون توزيع على union |
T extends (infer E)[] ? E : T | نوع عناصر المصفوفة |
T extends Promise<infer V> ? V : T | نوع قيمة promise |
T extends (...args: any[]) => infer R ? R : never | نوع القيمة المُعادة من دالة |
T extends [infer H, ...infer Rest] ? ... | العنصر الأول وبقية الـ tuple |
الأسئلة الشائعة
ما هو النوع الشرطي في TypeScript؟
نوع بالشكل T extends U ? X : Y. إذا كان T قابلًا للإسناد إلى U فالنتيجة X، وإلا فهي Y. إنه if/else للأنواع يُقيَّم وقت الترجمة، ولا يبقى منه شيء في JavaScript الناتج.
ماذا تفعل الكلمة المفتاحية infer في TypeScript؟
تعلن infer متغير نوع داخل جملة extends في النوع الشرطي، وتدع TypeScript تملؤه من النوع المطابَق. فالنوع T extends Promise<infer V> ? V : T يستخرج نوع القيمة المحلولة من نوع promise. ولا يمكن استخدامها إلا في جملة extends من نوع شرطي.
ما هو النوع الشرطي التوزيعي؟
عندما يكون النوع المفحوص معامل نوع مجردًا وتمرر إليه union، يُطبَّق الشرط على كل عضو منفصلًا ثم تُجمع النتائج. مع type ToArray<T> = T extends unknown ? T[] : never يصبح ToArray<string | number> هو string[] | number[]. لإيقاف ذلك ضع طرفي الاختبار بين قوسين مربعين: [T] extends [unknown].
كيف أحصل على نوع القيمة المُعادة من دالة في TypeScript؟
استخدم ReturnType<typeof fn> المدمج. إنه نوع شرطي مع infer: T extends (...args: any) => infer R ? R : any. وللدوال غير المتزامنة غلّفه بـ Awaited<...> لتحصل على القيمة المحلولة بدلًا من promise.
لماذا يعيد IsNever<never> النوع never بدلًا من true؟
never هو union فارغ، والنوع الشرطي التوزيعي يمر على أعضاء union. وبلا أعضاء لا يوجد ما يُمر عليه، فتكون النتيجة never. اكتب [T] extends [never] ? true : false لتختبر never نفسه.