نسخة مطبوعة على الآلة الكاتبة. السلطة أبدا

عندما رأيت الكلمة لأول مرة أبدًا ، فكرت في عدم ظهور النوع في TypeScript. مع مرور الوقت ، غرقت في عمق ts ، بدأت أفهم مدى قوة هذه الكلمة. وتولد هذه القوة من حالات الاستخدام في العالم الحقيقي التي أعتزم مشاركتها مع القارئ. من يهتم ، مرحبا بكم في القط.

ما ليس ابدا


إذا غرقنا في التاريخ ، فسنرى أن النوع لم يظهر أبدًا في فجر الإصدار 2.0 من برنامج TypeScript ، مع وصف بسيط إلى حد ما للغرض منه. إذا قمت باختيار إصدار مطوري ts لفترة وجيزة وحرية ، فإن النوع لن يكون أبدًا نوعًا بدائيًا يمثل علامة للقيم التي لن تحدث أبدًا. أو ، علامة للوظائف التي لن تُرجع القيم أبدًا ، إما بسبب الحلقة الخاصة بها ، على سبيل المثال ، حلقة لا نهائية ، أو بسبب انقطاعها. ولإظهار جوهر ما قيل بوضوح ، أقترح رؤية مثال أدناه:

/**    */ function error(message: string): never { throw new Error(message); } /**   */ function infiniteLoop(): never { while (true) { } } /**   */ function infiniteRec(): never { return infiniteRec(); } 

ربما بسبب هذه الأمثلة ، حصلت على الانطباع الأول بأن النوع مطلوب للوضوح.

اكتب النظام


الآن أستطيع أن أقول أن الحيوانات الغنية بالنوع النظام في TypeScript كذلك لا يعود أبدا. ودعماً لكلماتي ، سأقدم العديد من أنواع المكتبات من lib.es5.d.ts

 /** Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T; /** Extract from T those types that are assignable to U */ type Extract<T, U> = T extends U ? T : never; /** Construct a type with the properties of T except for those in type K. */ type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; /** Exclude null and undefined from T */ type NonNullable<T> = T extends null | undefined ? never : T; /** Obtain the parameters of a function type in a tuple */ type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; 

من بين أنواعي التي لا تستخدمها أبدًا ، سأعطي المفضل لدي - GetNames ، تناظرية محسنة لـ keyof:

 /** * GetNames      * @template FromType  -   * @template KeepType   * @template Include       .   false -    KeepType */ type GetNames<FromType, KeepType = any, Include = true> = { [K in keyof FromType]: FromType[K] extends KeepType ? Include extends true ? K : never : Include extends true ? never : K }[keyof FromType]; //   class SomeClass { firstName: string; lastName: string; age: number; count: number; getData(): string { return "dummy"; } } // be: "firstName" | "lastName" type StringKeys = GetNames<SomeClass, string>; // be: "age" | "count" type NumberKeys = GetNames<SomeClass, number>; // be: "getData" type FunctionKeys = GetNames<SomeClass, Function>; // be: "firstName" | "lastName" | "age" | "count" type NonFunctionKeys = GetNames<SomeClass, Function, false>; 

مراقبة التغيرات المستقبلية


عندما لا تكون حياة المشروع سريعة الزوال ، يأخذ التطوير طابعًا خاصًا. أرغب في التحكم في النقاط الرئيسية في التعليمات البرمجية ، للحصول على ضمانات بأنك أنت أو أعضاء فريقك لن تنسوا إصلاح جزء أساسي من الشفرة عندما يستغرق الأمر وقتًا. تظهر مقاطع التعليمات البرمجية هذه بشكل خاص عند وجود حالة أو تعداد لشيء ما. على سبيل المثال ، سرد مجموعة من الإجراءات على كيان. أقترح النظر في مثال لفهم سياق ما يحدث.

 //    -   ,     ,   ActionEngine type AdminAction = "CREATE" | "ACTIVATE"; // ,     ,   AdminAction   . class ActionEngine { doAction(action: AdminAction) { switch (action) { case "CREATE": //   return "CREATED"; case "ACTIVATE": //   return "ACTIVATED"; default: throw new Error("   "); } } } 

يتم تبسيط الرمز أعلاه قدر الإمكان فقط من أجل التركيز على نقطة مهمة - يتم تعريف نوع AdminAction في مشروع آخر ، وحتى أنه من غير الممكن أن يرافقه فريقك. نظرًا لأن المشروع سيعيش لفترة طويلة ، فمن الضروري حماية ActionEngine من التغييرات في نوع AdminAction دون علمك. يقدم برنامج TypeScript العديد من الوصفات لحل هذه المشكلة ، أحدها استخدام النوع أبدا. للقيام بذلك ، نحتاج إلى تعريف NeverError واستخدامه في طريقة doAction.

 class NeverError extends Error { //         - ts   constructor(value: never) { super(`Unreachable statement: ${value}`); } } class ActionEngine { doAction(action: AdminAction) { switch (action) { case "CREATE": //   return "CREATED"; case "ACTIVATE": //   return "ACTIVATED"; default: throw new NeverError(action); // ^       switch  . } } } 

أضف الآن قيمة "BLOCK" جديدة إلى AdminAction واحصل على خطأ في وقت الترجمة: وسيطة النوع "" BLOCK "" غير قابلة للتخصيص لمعلمة type 'never'.ts (2345).

من حيث المبدأ ، حققنا هذا. تجدر الإشارة إلى نقطة مثيرة للاهتمام وهي أن بناء التبديل يحمينا من تغيير عناصر AdminAction أو إزالتها من المجموعة. من الممارسة ، أستطيع أن أقول أنه يعمل حقًا كما هو متوقع.

إذا كنت لا ترغب في تقديم فئة NeverError ، فيمكنك التحكم في الكود عن طريق التصريح عن متغير النوع أبدًا. مثل هذا:

 type AdminAction = "CREATE" | "ACTIVATE" | "BLOCK"; class ActionEngine { doAction(action: AdminAction) { switch (action) { case "CREATE": //   return "CREATED"; case "ACTIVATE": //   return "ACTIVATED"; default: const unknownAction: never = action; // Type '"BLOCK"' is not assignable to type 'never'.ts(2322) throw new Error(`   ${unknownAction}`); } } } 

حد السياق: هذا + أبداً


الخدعة التالية غالباً ما تنقذني من الأخطاء السخيفة وسط التعب أو الإهمال. في المثال أدناه ، لن أقدم تقييماً لجودة النهج المختار. معنا ، الأمر الواقع ، يحدث هذا. افترض أنك تستخدم أسلوبًا في فصل دراسي لا يملك حق الوصول إلى حقول الفصل الدراسي. نعم ، يبدو الأمر مخيفًا - كل هذا رمز حكومي.

 @SomeDecorator({...}) class SomeUiPanel { @Inject private someService: SomeService; public beforeAccessHook() { //    ,    ,     SomeUiPanel this.someService.doInit("Bla bla"); // ^       :  beforeAccessHook   ,      } } 

في حالة أوسع ، يمكن أن تكون وظائف رد الاتصال أو السهم ، والتي لها سياقات التنفيذ الخاصة بها. والمهمة هي: كيف تحمي نفسك من أخطاء وقت التشغيل؟ لهذا ، TypeScript لديه القدرة على تحديد هذا السياق.

 @SomeDecorator({...}) class SomeUiPanel { @Inject private someService: SomeService; public beforeAccessHook(this: never) { //    ,    ,     SomeUiPanel this.someService.doInit("Bla bla"); // ^ Property 'someService' does not exist on type 'never' } } 

في الإنصاف ، سأقول أنه لا يستحق أبدًا. بدلاً من ذلك ، يمكنك استخدام باطل و {}. ولكن هذا لن يحدث أبدًا عندما تقرأ الشفرة التي تجذب الانتباه.

توقعات


الثوابت


امتلاك فكرة مؤكدة عن عدم الظهور أبدًا ، أعتقد أن الشفرة التالية يجب أن تعمل:

 type Maybe<T> = T | void; function invariant<Cond extends boolean>(condition: Cond, message: string): Cond extends true ? void : never { if (condition) { return; } throw new Error(message); } function f(x: Maybe<number>, c: number) { if (c > 0) { invariant(typeof x === "number", "When c is positive, x should be number"); (x + 1); // works because x has been refined to "number" } } 

لكن للأسف. يؤدي التعبير (x + 1) إلى حدوث خطأ: لا يمكن تطبيق عامل التشغيل "+" على النوعين "ربما" و "1". المثال نفسه أنا تجسست في المقالة نقل 30،000 سطر من التعليمات البرمجية من Flow إلى TypeScript.

ملزمة مرنة


اعتقدت أنه بمساعدة من لا أستطيع أبداً التحكم في معلمات الوظيفة الإلزامية ، وفي ظل ظروف معينة ، قم بتعطيل تلك غير الضرورية. لكن لا ، هذا لن ينجح:

 function variants<Type extends number | string>(x: Type, c: Type extends number ? number : never): number { if (typeof x === "number") { return x + c; } return +x; } const three = variants(1, 2); // ok // 2  - never,     string. ,   const one = variants("1"); // expected 2 arguments, but got 1.ts(2554) 

يتم حل المشكلة المذكورة أعلاه بطريقة مختلفة .

تحقق أكثر صرامة


أردت أن لا يفوت المترجم ts شيئًا من هذا القبيل يتعارض مع المنطق السليم.

 variants(<never> {}, <never> {}); 

استنتاج


في النهاية ، أود أن أقدم مهمة صغيرة ، من سلسلة من الغرائب ​​الغريبة. أي خط هو الخطأ؟

 class never<never> { never: never; } const whats = new never<string>(); whats.never = ""; 

خيار
في الأخير: النوع "" "" غير قابل للتعيين للكتابة "أبدًا". (2322)

هذا كل ما أردت أن أخبر عنه أبداً. شكرا لكم جميعا على اهتمامكم ونراكم قريبا.

Source: https://habr.com/ru/post/ar471026/


All Articles