Kodokon kodokon.com

الأنواع الشرطية وinfer: منطق المُصرِّف

أتقِن توزيعية الأنواع الشرطية، ومطابقة الأنماط بواسطة infer، والحالات الحدّية مثل never وany وboolean.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

الأنواع الشرطية هي آلية التفرّع في نظام الأنواع: T extends U ? X : Y تسأل المُصرِّف عمّا إذا كان T قابلاً للإسناد إلى U، ثم تختار فرعاً. جميع الأدوات القياسية مثل ReturnType أو Exclude أو Awaited مبنيّة عليها. في هذا المستوى، لم يعد السؤال كيف تكتبها، بل كيف تتوقّع بدقّة الطريقة التي يُقيِّمها بها المُصرِّف، خصوصاً عندما تتدخّل الاتّحادات وnever.

TYPESCRIPT
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;
// true

type B = IsString<string | number>;
// boolean, not false!
نتيجة مفاجئة على اتّحاد: boolean.

السلوك الأساسي هو التوزيعية: عندما يكون النوع المُختبَر بارامتر نوع مجرّداً (غير ملفوف داخل صف tuple أو مصفوفة أو تقاطع)، يُطبَّق الشرط على كل عضو من أعضاء الاتّحاد على حدة، ثم تُجمَع النتائج. لذلك يحسب IsString<string | number> القيمة true | false، أي boolean. ولتعطيل هذا التوزيع، لُفّ الطرفين داخل صف tuple: [T] extends [U]. حالة حدّية مخيفة: never هو الاتّحاد الفارغ؛ التوزيع على صفر أعضاء لا يُقيِّم أي فرع إطلاقاً، وتكون النتيجة never أياً كان الفرع الذي كتبته.

TYPESCRIPT
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

type B = NoDist<string | number>;
// (string | number)[]

type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true thanks to the tuple
الصف tuple يُعطِّل التوزيع ويجعل never موثوقاً.

تُدخِل الكلمة المفتاحية infer متغيّر نوع يُلتقَط عبر مطابقة الأنماط: يحاول المُصرِّف مطابقة بنية النوع المُختبَر مع النمط، ويربط المتغيّر إذا نجحت المطابقة. يمكنك وضع عدّة تصريحات infer بالاسم نفسه: في موضع تغايري مشترك (قيم مُنتَجة، مثل نوع الإرجاع)، تُدمَج المُرشّحات في اتّحاد؛ وفي موضع تغايري معاكس (بارامترات دالّة)، تُدمَج في تقاطع. تنبثق هذه القاعدة مباشرة من التغايرية: إنها الخيار الآمن الوحيد في كل اتّجاه.

TYPESCRIPT
type ElementOf<T> =
  T extends readonly (infer E)[] ? E : never;

type FirstParam<T> =
  T extends (arg: infer P) => unknown ? P : never;

type Merged<T> =
  T extends { a: (x: infer P) => void;
              b: (x: infer P) => void }
    ? P : never;

type M = Merged<{
  a: (x: { id: string }) => void;
  b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }
تصريحان infer في موضع تغايري معاكس: تقاطع.

منذ TypeScript 4.7، تقبل infer قيداً مضمّناً: infer N extends number يُرشِّح الالتقاط، ومنذ الإصدار 4.8 يحوّل المُصرِّف حتى سلسلة رقمية حرفية إلى عدد حرفي. دقّة أخرى في المواصفات: يبقى النوع الشرطي الذي ما يزال مُدخَله يحتوي بارامتراً عامّاً غير محلول مؤجَّلاً؛ ولا يُقيَّم إلا في موضع التنصيب، وهذا ما يفسّر لماذا تظهر بعض الأخطاء فقط من جانب المُستدعي.

TYPESCRIPT
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">;   // never
قيد على infer وتحويل حرفي (4.8).

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. ما هو النوع Exclude<boolean, true>؟
    • boolean
    • false
    • never
    • true
  2. مع type IsNever<T> = T extends never ? true : false;، ما هو IsNever<never>؟
    • true
    • false
    • never
    • boolean
  3. تصريحان infer P بالاسم نفسه موضوعان في موضع بارامتر دالّة يُنتجان:
    • اتّحاداً للمُرشّحات
    • تقاطعاً للمُرشّحات
    • أول مُرشّح جرى لقاؤه
    • خطأ تصريف