never هو النوع الذي لا قيم له. الدالة التي نوع قيمتها المُعادة never لا تعود أبدًا بشكل طبيعي: ترمي خطأ أو تعمل إلى الأبد. والمتغير من نوع never يعلّم كودًا لا يمكن أن يعمل، وهذا ما يجعل الفحوص الشاملة ممكنة.
يُترجم return fail(...) في دالة تعيد number لأن never يمكن إسناده إلى كل نوع: قيمة من نوع never لا يمكن أن تصل فعليًا.
الدوال التي لا تعود أبدًا
نوعان من الدوال لا يبلغان نهايتهما أبدًا: دالة ترمي خطأ دائمًا، ودالة فيها حلقة لا نهائية. يتحقق المترجم من هذا الادعاء. الدالة المعلنة بنوع never ويمكن الوصول إلى نهايتها هي الخطأ TS2534، A function returning 'never' cannot have a reachable end point.
function fail(message: string): never {
throw new Error(message);
}
function runForever(): never {
while (true) {
// poll, serve requests...
}
}
يختلف الاستنتاج بحسب الصياغة. تصريح الدالة (function declaration) الذي لا يفعل سوى رمي خطأ يُستنتج أنه يعيد void، بينما الدالة السهمية أو تعبير الدالة الذي لا يفعل سوى رمي خطأ يُستنتج أنه never:
function f1() { throw new Error("x"); } // () => void
const f2 = () => { throw new Error("x"); }; // () => never
لا يعامل التضييق الاستدعاء كطريق مسدود إلا عندما يكون للاسم المستدعى نوع صريح يعيد never: تصريح دالة معلن بـ : never، مثل fail أعلاه، أو متغير له تعليق نوع، const fail: (m: string) => never = (m) => { throw new Error(m); }. الـ never المستنتج لا يُحتسب، ولا const fail = (m: string): never => ... كذلك، حيث حُدد نوع الدالة السهمية فقط لا المتغير.
never مقابل void
void | never | |
|---|---|---|
| هل تنتهي الدالة | نعم | لا (ترمي خطأ أو تدور إلى الأبد) |
| القيمة وقت التشغيل | undefined | لا شيء: الاستدعاء لا ينتج قيمة أبدًا |
| الكود بعد الاستدعاء | قابل للوصول | غير قابل للوصول |
| إمكانية إسناده إلى أنواع أخرى | إلى void وunknown وany فقط | إلى كل نوع |
| الاستخدام المعتاد | callbacks ومعالجات الأحداث والدوال ذات الآثار الجانبية | fail() وassertNever() والحلقات اللانهائية |
يظهر الفرق العملي في التضييق. بعد if (!user) fail("no user")، يعرف المترجم أن user معرّف في السطر التالي فقط إذا كانت fail تعيد never. ومع قيمة مُعادة void يفترض أن التنفيذ قد يستمر.
الفحص الشامل باستخدام never
كل case في switch على union يضيّق القيمة. عندما تُعالج كل الأعضاء، يكون ما تبقى في default هو never. إسناده إلى متغير never يحوّل «عالجت كل الحالات» إلى شيء يتحقق منه المترجم:
أضف الآن عضوًا ثالثًا إلى الـ union دون إضافة case:
index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.
يسمّي الخطأ العضو الذي نسيته. أضف case "triangle": return (shape.base * shape.height) / 2; فيُترجم الكود من جديد. الدالة المساعدة assertNever هي الصيغة القابلة لإعادة الاستخدام من الفحص نفسه، وما زال throw فيها مهمًا وقت التشغيل: البيانات القادمة من JSON أو من عميل أقدم قد تحتوي على kind تقول الأنواع إنه مستحيل. هذا النمط هو العمود الفقري لـ discriminated unions.
التضييق حتى never
يحدث الشيء نفسه مع أي تضييق، لا مع switch فقط. بمجرد استبعاد كل احتمال، يصبح نوع المتغير never:
إذا وسّعت المعامل لاحقًا إلى string | number | boolean | bigint، يصبح السطر const nothing: never = x خطأ، يشير إلى الدالة التي تحتاج إلى تحديث.
never يختفي داخل union
never هو المجموعة الفارغة من القيم، لذلك لا تغيّر إضافته إلى union شيئًا: string | never هو ببساطة string. وهكذا تصفّي الأنواع الشرطية الـ unions. الفرع الذي يعيد never يحذف ذلك العضو:
يعمل Exclude<T, U> وExtract<T, U> المدمجان بهذه الطريقة بالضبط. أما في intersection فالعكس: string & never هو never.
الأنواع المستحيلة تصبح never
الـ intersection الذي لا تستطيع أي قيمة تحقيقه يُختزل إلى never:
type A = string & number; // never
type B = { kind: "a" } & { kind: "b" }; // never
قراءة خاصية من قيمة B تبلّغ عن السبب: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. عندما يتبيّن أن نوعًا بنيته هو never، ابحث عن جزأين يتناقضان.
never وunknown وany
| النوع | القيم التي يحملها | يمكن إسناده إلى | يقبل |
|---|---|---|---|
unknown | كل القيم | unknown وany فقط | كل شيء |
any | كل القيم | كل شيء ما عدا never | كل شيء |
never | لا قيم | كل شيء | never فقط |
unknown قمة تسلسل الأنواع وnever قاعه. أما any فليس جزءًا من التسلسل أصلًا: إنه يوقف الفحوص.
الأسئلة الشائعة
ما هو النوع never في TypeScript؟
never هو النوع الذي لا قيم له. لا يمكن إسناد أي شيء إليه (إلا never آخر)، ويمكن إسناده إلى كل نوع. يظهر كنوع القيمة المُعادة للدوال التي ترمي خطأ دائمًا أو تدور إلى الأبد، وكنوع المتغير بعد أن يستبعد التضييق كل الاحتمالات، وكناتج للأنواع المستحيلة مثل string & number.
ما الفرق بين never و void؟
الدالة التي تعيد void تنتهي بشكل طبيعي؛ هي فقط لا تعيد قيمة مفيدة (وقت التشغيل تعيد undefined). أما الدالة التي تعيد never فلا تنتهي إطلاقًا: ترمي خطأ أو تعمل إلى الأبد. الكود بعد استدعاء دالة never غير قابل للوصول، وتعامله TypeScript على هذا الأساس عند التضييق.
كيف أجري فحصًا شاملًا في TypeScript؟
في فرع default من switch على union، أسند القيمة إلى متغير نوعه never، أو مررها إلى دالة assertNever(value: never): never ترمي خطأ. إذا عولجت كل الحالات، تكون القيمة هناك never ويُترجم الكود. وإذا غابت حالة، يبلّغ المترجم أن العضو الغائب لا يمكن إسناده إلى never.
لماذا أصبح نوعي never؟
غالبًا لأن TypeScript ضيّقت كل الخيارات (مثلًا بعد فحص typeof x === "string" وtypeof x === "number" على string | number)، أو لأن intersection مستحيل: string & number، أو نوعا كائن لخاصيتهما المشتركة أنواع حرفية متعارضة. مرّر المؤشر فوق النوع في المحرر لترى أي خطوة أنتجته.
ماذا تعني الرسالة "is not assignable to type never"؟
حاول الكود وضع قيمة حقيقية حيث لا يُسمح إلا بـ never. في الفحص الشامل تعني أن أحد أعضاء union لم يُعالج. وفي غير ذلك تعني غالبًا أن مصفوفة استُنتجت كـ never[] أو أن intersection انهار إلى never؛ أضف تعليق نوع أو أصلح الأنواع المتعارضة.