مدرسة TypeScript of Magic: Generics and Type Extension

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

بمرور الوقت ، أدرك مؤلف هذه المادة أنه في كل مرة يتجاوز فيها الأخطاء المكتشفة في مرحلة التجميع ، يضع قنبلة موقوتة في التعليمات البرمجية الخاصة به يمكن أن تنفجر أثناء تنفيذ البرنامج. في كل مرة كان "يكافح" مع الأخطاء باستخدام بسيطة as any بناء ، كان عليه أن يدفع ثمن ذلك مع ساعات طويلة من تصحيح الأخطاء.



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

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

تركز هذه المواد على حالتين. من خلال تجنب استخدام any نوع فيها ، يمكنك ضمان سلامة نوع الشفرة ، وفتح إمكانيات إعادة استخدامها ، وجعلها بديهية.

الوراثة


افترض أننا نعمل على قاعدة بيانات مدرسة. كتبنا getBy وظيفة مساعد مريحة للغاية. من أجل الحصول على الكائن الذي يمثل الطالب باسمه ، يمكننا استخدام أمر النموذج getBy(model, "name", "Harry") . دعونا نلقي نظرة على تنفيذ هذه الآلية (حتى لا يتم تعقيد الشفرة ، يتم تمثيل قاعدة البيانات بمصفوفة عادية).

 type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) {   return model.filter(item => item[prop] === value)[0] } 

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

 function getBy(model: Student[], prop: string, value): Student | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student 

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

في TypeScript ، كما هو الحال في اللغات الأخرى المكتوبة بشدة ، يمكننا استخدام الأدوية العامة ، والتي تسمى أيضًا "الأنواع العامة" و "الأنواع العامة" و "التعميمات".

يشبه عام متغير عادي ، ولكن بدلاً من بعض القيمة ، فإنه يحتوي على تعريف نوع. نعيد كتابة كود وظيفتنا بحيث يستخدم النوع العالمي T بدلاً من نوع Student T

 function getBy<T>(model: T[], prop: string, value): T | null {   return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student 

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

حتى الآن لدينا وظيفة مساعد موثوق بها مناسبة لإعادة الاستخدام. ومع ذلك ، لا يزال من الممكن تحسينه. ماذا لو حدث خطأ عند إدخال المعلمة الثانية وبدلاً من "name" يبدو أن هناك "naem" ؟ سوف تتصرف الوظيفة كما لو أن الطالب الذي تبحث عنه ليس موجودًا في قاعدة البيانات ببساطة ، والأكثر إزعاجًا أنه لن ينتج عنه أي أخطاء. يمكن أن يؤدي هذا إلى تصحيح الأخطاء على المدى الطويل.

للحماية من مثل هذه الأخطاء ، نقدم نوعًا عالميًا آخر ، P في هذه الحالة ، من الضروري أن تكون P مفتاحًا من النوع T ، لذلك ، إذا Student استخدام Student هنا ، فمن الضروري أن تكون P هي السلسلة "name" أو "age" أو "hasScar" . إليك كيفية القيام بذلك.

 function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'. 

يعد استخدام الأدوية العامة keyof حيلة قوية جدًا. إذا قمت بكتابة برامج في IDE تدعم TypeScript ، فعند إدخال الوسيطات ، يمكنك الاستفادة من إمكانات الإكمال التلقائي ، وهو أمر مريح للغاية.

ومع ذلك ، لم ننتهي من العمل على وظيفة getBy حتى الآن. لديها حجة ثالثة ، لم نضع نوعها بعد. هذا لا يناسبنا على الإطلاق. حتى الآن ، لم نتمكن من معرفة نوعه مسبقًا ، لأنه يعتمد على ما نمرره كوسيطة ثانية. ولكن الآن ، بما أن لدينا النوع P ، يمكننا استنتاج ديناميكيًا نوع الوسيطة الثالثة. سيكون نوع الوسيطة الثالثة في النهاية T[P] . ونتيجة لذلك ، إذا كان T هو Student ، و P هو "age" ، فسيكون T[P] من النوع number .

 function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //      

آمل أن يكون لديك الآن فهم واضح تمامًا لكيفية استخدام الأدوية الجنسية في TypeScript ، ولكن إذا كنت ترغب في تجربة جيدة مع كل ما تريد تجربته مع الشفرة التي تمت مناقشتها هنا ، يمكنك إلقاء نظرة هنا .

توسيع الأنواع الموجودة


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

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

إذا حاولنا إضافة وظيفة إلى النموذج الأولي Array ، فلن يحب المترجم هذا كثيرًا:

 Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") //  ...   ? 

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

سيكون من الأفضل توسيع نوع Array ، ولكن قبل القيام بذلك ، دعنا نتحدث عن كيفية معالجة TypeScript للمواقف عند وجود واجهتين من نفس النوع في التعليمات البرمجية. هنا يتم تطبيق مخطط بسيط للعمل. سيتم دمج الإعلانات ، إن أمكن. إذا لم تتمكن من دمجها ، فسوف يعطي النظام خطأ.

لذلك يعمل هذا الرمز:

 interface Wand { length: number } interface Wand {   core: string } const myWand: Wand = { length: 11, core: "phoenix feather" } //  ! 

وهذا ليس:

 interface Wand { length: number } interface Wand {   length: string } // Error: Subsequent property declarations must have the same type.  Property 'length' must be of type 'number', but here has type 'string'. 

الآن ، بعد التعامل مع هذا ، نرى أننا نواجه مهمة بسيطة إلى حد ما. أي أن كل ما نحتاجه هو الإعلان عن واجهة Array<T> وإضافة وظيفة getBy إليها.

 interface Array<T> {  getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); //   ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //     

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

 declare global {   interface Array<T> {       getBy<P extends keyof T>(prop: P, value: T[P]): T | null;   } } 

إذا كنت ستقوم بتوسيع واجهة مكتبة خارجية ، فمن المحتمل أنك ستحتاج إلى الوصول إلى namespace هذه المكتبة. فيما يلي مثال على كيفية إضافة حقل userId إلى Request من مكتبة Express :

 declare global { namespace Express {   interface Request {     userId: string;   } } } 

يمكنك تجربة الكود في هذا القسم هنا .

الملخص


في هذه المقالة ، نظرنا في تقنيات استخدام الأدوية البديلة وامتدادات الكتابة في TypeScript. نأمل أن يساعدك ما تعلمته اليوم في كتابة رمز موثوق به ومفهوم وآمن للنوع.

أعزائي القراء! ما هو شعورك تجاه أي نوع في TypeScript؟

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


All Articles