التحقق من صحة واجهة TypeScript باستخدام Joi

قصة كيفية قضاء يومين إعادة كتابة نفس الرمز عدة مرات.


Joi & TypeScript. A love story


دخول


في هذه المقالة ، سأحذف تفاصيل حول Hapi و Joi والتوجيه validate: { payload: ... } ، مما يعني ضمنيًا أنك تفهم بالفعل ما هو عليه ، وكذلك المصطلحات ، "واجهات" و "أنواع" وما شابه ذلك. . لن أخبركم سوى عن استراتيجية تعتمد على الدور وليس أكثرها نجاحًا ، ألا وهي تدريبي في هذه الأمور.


خلفية صغيرة


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


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


المشكلة


سيكون ملف تعريف المستخدم البسيط مثالاً مجردة.


  • أولا خطوة الصفر لمطور جيد: وصف البيانات اكتب اختبارات
  • الخطوة الأولى: اكتب الاختبارات وصف البيانات
  • و هكذا.

لنفترض أن الاختبارات قد تمت كتابتها بالفعل لهذا الرمز ، يبقى أن يصف البيانات:


 interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; 

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


 const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

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


لا يمكننا ببساطة تحديد const joiUserValidator: IUser ، لأن Joi يستخدم أنواع البيانات الخاصة به ، مما يولد أخطاء عند تجميع النوع Type 'NumberSchema' is not assignable to type 'number' . ولكن يجب أن يكون هناك طريقة لأداء التحقق من الصحة على الواجهة؟


ربما لم أكن جوجل جوجل بشكل صحيح ، أو درست الإجابات بشكل سيئ ، ولكن كل القرارات جاءت extractTypes أنواع extractTypes من الدراجات الشرسة ، مثل هذا :


 type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : /* ... more schemata ... */ never; 

قرار


استخدم مكتبات الجهات الخارجية


لم لا. عندما سألت الناس عن مهمتي ، تلقيت في واحدة من الإجابات ، وبعد ذلك ، وهنا ، في التعليقات (بفضل keenondrums ) ، روابط لهذه المكتبات:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer


ومع ذلك ، كان هناك اهتمام لمعرفة ذلك بنفسك ، لفهم عمل TS بشكل أفضل ، ولم يكن هناك شيء يحث على حل المشكلة في لحظات.


الحصول على جميع الخصائص


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


 interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

يتيح لك TypeScript ظل ظروف صعبة وغامضة الحصول على ، على سبيل المثال ، مفاتيح من الواجهة ، كما لو كان كائن JS عاديًا ، ومع ذلك ، فقط في بنية type ومن خلال key in keyof T وفقط من خلال الوراثة. كنتيجة لنوع UserKeys ، يجب أن يكون لكل الكائنات التي تقوم بتنفيذ الواجهات نفس مجموعة الخصائص ، لكن أنواع القيم يمكن أن تكون عشوائية. يتضمن ذلك تلميحات في IDE ، ولكن لا يزال لا يعطي إشارة واضحة لأنواع القيم.


هنا حالة أخرى مثيرة للاهتمام لم أستطع استخدامها. ربما يمكنك أن تخبرني لماذا هذا ضروري (على الرغم من أنني أخمن جزئيًا ، لا يوجد مثال تطبيقي كافٍ):


 interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

استخدام أنواع متغير


يمكنك تعيين الأنواع بشكل صريح ، واستخدام "OR" واستخراج الخصائص ، والحصول على رمز عمل محلي:


 type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

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


 const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10; // error TS2362: The left-hand side of an arithmetic operation must be of type'any', 'number', 'bigint' or an enum type 

يأتي هذا الخطأ من Joi.NumberSchema لأنه لا يمكن أن يكون العمر مجرد number . ما قاتلوا من أجل وركض إلى.


الجمع بين حلين في واحد؟


في مكان ما في هذه المرحلة ، كان يوم العمل يقترب من نهايته المنطقية. أخذت نفسًا وشربت القهوة ومحو اللعنة. من الضروري قراءة هذه الإنترنت لديك بشكل أقل! لقد حان الوقت خذ بندقية و العقول poraskinut:


  1. يجب تكوين كائن مع أنواع قيمة واضحة ؛
  2. يمكنك استخدام الأدوية العامة لرمي الأنواع في واجهة واحدة ؛
  3. الأدوية الجنيسة تدعم الأنواع الافتراضية ؛
  4. type البناء قادر بوضوح على شيء آخر.

نكتب الواجهة العامة مع الأنواع الافتراضية:


 interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; } 

بالنسبة لـ Joi ، يمكنك إنشاء واجهة ثانية ، وراثة الواجهة الرئيسية بهذه الطريقة:


 interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {} 

ليست جيدة بما يكفي ، لأن المطور التالي يمكنه توسيع IUserJoi بقلب خفيف أو أسوأ من ذلك. خيار أكثر محدودية هو الحصول على سلوك مماثل:


 type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>; 

نحن نحاول:


 const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

UPD:
في Joi.object في Joi.object اضطررت للقتال مع الخطأ TS2345 وكان الحل الأكثر بساطة هو as any . أعتقد أن هذا ليس افتراضًا مهمًا ، لأن الكائن أعلاه لا يزال على الواجهة.


 const joiUserInfo = { info: Joi.object(joiUser as any).required() }; 

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


تلخيص


ما هي الاستنتاجات التي يمكن استخلاصها من كل هذا:


  1. من الواضح أنني لم أتعلم كيفية العثور على إجابات للأسئلة. بالتأكيد ، مع طلب ناجح ، يكون هذا الحل (أو الأفضل) في أول 5 كيلو بايت من محرك البحث ؛
  2. إن التحول إلى التفكير الثابت من الديناميكية ليس بالأمر السهل ، وغالبًا ما أقوم بالتطويق على مثل هذا الاحتشاد ؛
  3. الأدوية الجنيسة رائعة. على Habr و stackoverflow ممتلئ الدراجات حلول غير واضحة لبناء كتابة قوية ... خارج وقت التشغيل.

ما فزنا به:


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

الأخلاقية: التجربة لا تقدر بثمن ؛ أما بالنسبة للباقي ، فهناك خريطة للعالم.


يمكنك أن ترى ، المس ، النتيجة النهائية:
https://repl.it/@Melodyn/Joi-by-interface

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


All Articles