لقد امتلكنا الآن بعض المفاهيم الأساسية عن عمل TypeScript وعن كيفية استخدامها في إنشاء المشاريع الصغيرة، وأصبحنا قادرين على تنفيذ أشياء مفيدة فعلًا. سنبدأ إذًا بإنشاء مشروع جديد ذو استخدامات أكثر واقعية.
سيكون التغيير الرئيسي في هذا القسم عدم استخدام ts-node. وعلى الرغم من أنها أداة مفيدة وتساعدك في الانطلاق، إلا أن استخدام المصرِّف الرسمي للغة هو الخيار المفضل، وخاصة على المدى الطويل لأعمالك. يأتي هذا المصرِّف مع الحزمة typescript، حيث يولّد ويحزم ملفات JavaScript انطلاقًا من ملفات "ts." وبذلك لن تحتوي نسخة الإنتاج بعد ذلك شيفرات TypeScript. وهذا ما نحتاجه فعلًا لأن TypeScript غير قابلة للتنفيذ على المتصفح أو على Node.
إعداد المشروع
سننشئ مشروعًا من أجل Ilari الذي يحب وضع خطط لرحلات جوية قصيرة، لكنه يعاني من ذكريات مزعجة متعلقة بهذه الرحلات. سيكون هو نفسه من سيكتب شيفرة التطبيق فلن يحتاج إلى واجهة مستخدم، لكنه يرغب باستعمال طلبات HTTP ويبقى خياراته مفتوحة في إضافة واجهة مستخدم مبنية على تقنيات الويب لاحقًا.
لنبدأ بإنشاء أول مشروع واقعي لنا بعنوان Ilari flight diaries. وسنثبت كالعادة حزمة typescript باستخدام الأمر npm init
ومن ثم npm install typescript
.
يساعدنا مصرِّف TypeScript الأصلي tsc في تهيئة المشروع من خلال الأمر tsc --init
. لكن علينا أولًا أن نضيف الأمر tsc إلى قائمة السكربتات القابلة للتنفيذ داخل الملف package.json (إلا إن كنت قد ثبتَّ TypeScript لكافة المشاريع). وحتى لو ثبتّها لكافة المشاريع لابد من إدراجها كاعتمادية تطوير في مشروعك.
سيكون سكربت npm الذي سينفذ الأمر tsc كالتالي:
{ // .. "scripts": { "tsc": "tsc", }, // .. }
يضاف الأمر tsc غالبًا لاستخدامه في سكربتات تنفذ سكربتات أخرى، وبالتالي من الشائع رؤية إعداداته ضمن المشروع بهذا الشكل.
سنهيئ الآن إعدادات الملف tsconfig.json بتنفيذ الأمر:
npm run tsc -- --init
لاحظ وجود المحرفين "--" قبل الوسيط الفعلي init. يُفسَّر الوسيط الذي يقع قبل "--" كأوامر npm والذي يقع بعدها كأوامر تُنفَّذ من قبل السكربت.
ينتج عن تنفيذ الأمر السابق الملف tsconfig.ts الذي يضم قائمة طويلة بكل أوامر التهيئة التي يمكن ضبطها. وسترى أن قلة منها فقط لم تُسبق بعلامة التعليق. ستفيدك دراسة هذا الملف في إيجاد بعض تعليمات التهيئة التي قد تحتاجها. يمكنك أيضًا ترك الأسطر التي تبدأ بعلامة التعليق في الملف، فلربما قررت لاحقًا توسيع إعدادات التهيئة الخاصة بمشروعك.
إن كل ما نحتاج إليه حاليًا من إعدادات هي:
{ "compilerOptions": { "target": "ES6", "outDir": "./build/", "module": "commonjs", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "esModuleInterop": true } }
لنعرّج على الإعدادات واحدًا تلو الآخر:
- target: سيخبر المصرِّف عن نسخة ECMAScript التي سيستعملها لتوليد ملفات JavaScript. ولاحظ أن القيمة التي يأخذها هي ES6 التي تدعمها معظم المتصفحات، وهي خيار جيد وآمن.
- outDir: يحدد المجلد الذي سيحوي الشيفرة المصرِّفة.
-
modules: يخبر المصرِّف أننا سنستخدم وحدات commonjs في الشيفرة المصرِّفة. وهذا يعني أننا نستطيع استخدام
require
بدلًا منimport
وهذا الأمر غير مدعوم في النسخ الأقدم من Node مثل النسخة 10. -
strict: وهو في الواقع اختصار لعدة خيارات منفصلة توجه استخدام ميزات TypeScript بطريقة أكثر تشددًا (يمكنك أن تجد تفاصيل بقية الخيارات في توثيق tsconfig، كما ينصح التوثيق باستخدام strict دائمًا) هي:
- noImplicitAny: يكون الخيار المألوف بالنسبة لنا هو الأكثر أهمية. ويمنع هذا الخيار تضمين النوع any (إعطاء قيمة ما النوع any)، والذي قد يحدث إن لم تحدد نوعًا لمعاملات دالة على سبيل المثال.
- noImplicitThis
- alwayesStrict
- strictBindCallApply
- strictNullChecks
- strictFunctionTypes
- strictpropertyIntialization
- noUnUsedLocals: يمنع وجود متغيرات محلية غير مستعملة.
- nofallThroughCasesInSwitch: ترمي خطأً إن احتوت الدالة على معامل لم يستعمل.
-
esModuleInterop: تتأكد من وجود إحدى التعليمتين
return
أوbreak
في نهاية الكتلة case، عند استخدام البنية switch case. - ModuleInterop: تسمح بالتصريف بين وحدات commonjs و ES. يمكنك الاطلاع أكثر على هذا الإعداد في توثيق tsconfig
وبما أننا أنهينا ضبط إعدادات التهيئة التي نريد، سنتابع العمل بتثبيت المكتبة express وتثبيت تعريفات الأنواع الموجودة ضمنها مستخدمين "types/express@". وبما أن المشروع واقعي، أي أنه سينمو مع الوقت، سنستخدم المدقق eslint من البداية:
npm install express npm install --save-dev eslint @types/express @typescript-eslint/eslint-plugin @typescript-eslint/parser
ستبدو محتويات الملف package.json مشابهة للتالي:
{ "name": "ilaris-flight-diaries", "version": "1.0.0", "description": "", "main": "index.ts", "scripts": { "tsc": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1" }, "devDependencies": { "@types/express": "^4.17.2", "@typescript-eslint/eslint-plugin": "^2.17.0", "@typescript-eslint/parser": "^2.17.0", "eslint": "^6.8.0", "typescript": "^3.7.5" } }
كما سننشئ ملف تهيئة المدقق ذو اللاحقة eslintrc ونزوده بالمحتوى التالي:
{ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "plugins": ["@typescript-eslint"], "env": { "browser": true, "es6": true }, "rules": { "@typescript-eslint/semi": ["error"], "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-unused-vars": [ "error", { "argsIgnorePattern": "^_" } ], "@typescript-eslint/no-explicit-any": 1, "no-case-declarations": 0 }, "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" } }
سنتابع الآن العمل بإعداد بيئة التطوير، وسنصبح بعد ذلك جاهزين لكتابة الشيفرة الفعلية. هناك خيارات عدة لإجراء ذلك. فقد نستخدم المكتبة nodemon مع ts-node، لكن وكما رأينا سابقًا فالمكتبة ts-node-dev تؤمن الوظائف نفسها، لذا سنستمر في استخدامها:
npm install --save-dev ts-node-dev
سنعرّف بعض سكربتات npm:
{ // ... "scripts": { "tsc": "tsc", "dev": "ts-node-dev index.ts", "lint": "eslint --ext .ts ." }, // ... }
هنالك الكثير لتفعله قبل البدء بكتابة الشيفرة. فعندما تعمل على مشروع حقيقي، سيفيدك الإعداد المتأني لبيئة التطوير إلى حد كبير. خذ وقتك في ضبط إعدادات بيئة العمل لك ولفريقك، وسترى أن كل شيء سيسير بسلاسة على المدى الطويل.
لنبدأ بكتابة الشيفرة
سنبدأ كتابة الشيفرة بإنشاء وصلة تخديم endpoint لتفقد الخادم، لكي نضمن أن كل شيء يعمل بالشكل الصحيح.
فيما يلي محتوى الملف index.ts:
import express from 'express'; const app = express(); app.use(express.json()); const PORT = 3000; app.get('/ping', (_req, res) => { console.log('someone pinged here'); res.send('pong'); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
لو نفذنا الآن الأمر npm run dev
سنرى أن الطلب إلى العنوان http://localhost:3000/ping، سيعيد الاستجابة pong، وبالتالي فتهيئة المشروع قد أنجزت بالشكل الصحيح.
سيعمل التطبيق بعد تنفيذ الأمر السابق في وضع التطوير. وهذا الوضع لن يكون مناسبًا أبدًا عند الانتقال لاحقًا إلى وضع الإنتاج.
لنحاول إنجاز نسخة إنتاج بتشغيل مصرِّف TypeScript. وبما أننا حددنا قيمةً للإعداد outDir في الملف package.json، فلا شيء بقي لنفعله سوى تنفيذ السكربت باستخدام الأمر npm run tsc
.
ستكون النتيجة نسخة إنتاج مكتوبة بلغة JavaScript صرفة، لواجهة express الخلفية ضمن المجلد build.
سيفسر المدقق eslint حاليًًا الملفات ويضعها ضمن المجلد build نفسه. ولا نرغب بالطبع أن يحدث هذا، فمن المفترض أن تكون الشيفرة الموجودة في هذا المجلد ناتجة عن المصرِّف فقط. يمكن منع ذلك بإنشاء ملف تجاهل لاحقته "eslintignore." يضم قائمة بالمحتوى الذي نريد من المدقق أن يتجاهله، كما هي الحال مع git وgitignore.
لنضف سكربت npm لتشغيل التطبيق في وضع الإنتاج:
{ // ... "scripts": { "tsc": "tsc", "dev": "ts-node-dev index.ts", "lint": "eslint --ext .ts .", "start": "node build/index.js" }, // ... }
وبتشغيل التطبيق باستخدام الأمر npm start
، نتأكد أن نسخة الانتاج ستعمل أيضًا بشكل صحيح.
لدينا الآن بيئة تطوير تعمل بالحد الأدنى. وبمساعدة المصرِّف والمدقق eslint سنضمن أن تبقى الشيفرة بمستوًى جيد. وبناء على ما سبق سنتمكن من إنشاء تطبيق قابل للنشر لاحقًا كنسخ إنتاج.
التمرينان 9.8 - 9.9
قبل أن تبدأ بالحل
ستطوّر خلال مجموعة التمارين هذه واجهة خلفية لمشروع جاهز يدعى Patientor. والمشروع عبارة عن تطبيق سجلات طبية بسيط يستخدمه الأطباء الذين يشخصون الأمراض ويسجلون المعلومات الصحية لمرضاهم.
بُنيت الواجهة الأمامية مسبقًا من قبل خبراء خارجيين، ومهمتك أن تطور واجهة خلفية تدعم الشيفرة الموجودة.
9.8 الواجهة الخلفية لتطبيق إدارة المرضى: الخطوة 1
هيئ المشروع الذي ستستخدمه الواجهة الأمامية. ثم هيئ المدقق eslint والملف tsconfig بنفس إعدادات التهيئة التي استخدمناها سابقًا. عرِّف وصلة تخديم تستجيب لطلبات HTTP-GET المرسلة إلى الوجهة ping/.
ينبغي أن يعمل المشروع باستخدام سكربتات npm في وضع التطوير، وكشيفرة مصرّفة في وضع الإنتاج.
9.9 الواجهة الخلفية لتطبيق إدارة المرضى: الخطوة 2
انسخ المشروع patientor. شغل بعد ذلك التطبيق مستعينًا بالملف README.md. يجب أن تعمل الواجهة الأمامية دون حاجة لوجود واجهة خلفية وظيفية.
تأكد من استجابة الواجهة الخلفية إلى طلب التحقق من الخادم ping الذي ترسله الواجهة الأمامية. وتحقق من أداة التطوير للتأكد من سلامة عملها.
من الجيد أن تلقي نظرة على النافذة console في طرفية التطوير. وإن أخفق شيء ما، استعن بمعلومات القسم الثالث المتعلقة بحل هذه المشكلة.
كتابة شيفرة وظائف الواجهة الخلفية
لنبدأ بالأساسيات. يريد Ilari أن يكون قادرًا على مراجعة ما اختبره خلال رحلاته الجوية. إذ يريد تخزين مدخلات في مذكراته تتضمن المعلومات التالية:
- تاريخ المُدخل
- حالة الطقس (جيد، رياح، ماطر، عاصف)
- مدى الرؤية (جيد، ضعيف)
- نص يفصِّل تجربته ضمن الرحلة.
حصلنا على عينة من المعلومات التي سنستخدمها كأساس نبني عليه. خُزّنت المعلومات بصيغة JSON ويمكن الحصول عليها من GitHub.
تبدو البيانات على النحو التالي:
[ { "id": 1, "date": "2017-01-01", "weather": "rainy", "visibility": "poor", "comment": "Pretty scary flight, I'm glad I'm alive" }, { "id": 2, "date": "2017-04-01", "weather": "sunny", "visibility": "good", "comment": "Everything went better than expected, I'm learning much" }, // ... ]
لننشئ وصلة تخديم تعيد كل مدخلات مذكرة الرحلات.
علينا في البداية إتخاذ بعض القرارات المتعلقة بطريقة هيكلة الشيفرة المصدرية. فمن الأفضل في حالتنا وضع الشيفرة بأكملها في مجلد واحد باسم src، وهكذا لن تختلط ملفات الشيفرة مع ملفات التهيئة. إذًا سننقل الملف إلى هذا المجلد ونجري التعديلات اللازمة على سكربت npm.
سنضع أيضًا وحدات المتحكمات بالمسار routers المسؤولة عن التعامل مع موارد محددة كمدخلات المذكرات، ضمن المجلد src/routes. يختلف ما فعلناه قليلًا عن المقاربة التي اتبعناها في القسم 4 حيث وضعنا المتحكمات في المجلد src/controllers.
ستجد شيفرة المتحكم الذي يتعامل مع جميع وصلات تخديم المرتبطة بالمذكّرات في الملف "src/routes/diaries.ts" الذي يحتوي الشيفرة التالية:
import express from 'express'; const router = express.Router(); router.get('/', (_req, res) => { res.send('Fetching all diaries!'); }) router.post('/', (_req, res) => { res.send('Saving a diary!'); }) export default router;
سنوجّه كل الطلبات التي تبدأ بالعنوان api/diaries/ إلى متحكم المسار المحدد في الملف index.ts
import express from 'express'; import diaryRouter from './routes/diaries'; const app = express(); app.use(express.json()); const PORT = 3000; app.get('/ping', (_req, res) => { console.log('someone pinged here'); res.send('pong'); }); app.use('/api/diaries', diaryRouter); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
وهكذا لو أرسلنا طلبًا إلى العنوان http://localhost:3000/api/diaries فمن المفترض أن نرى الرسالة Fetching all diaries.
علينا تاليًا البدء بتقديم البيانات الأساسية (عينة المذكّرات) انطلاقًا من التطبيق. إذ سنحضر البيانات ونخزنها في الملف data/diaries.json.
لن نكتب الشيفرة التي تغير البيانات الفعلية في ملف المتحكم بالمسار، بل سننشئ خدمة تهتم بهذا الأمر. ويعتبر فصل "منطق العمل" عن شيفرة متحكمات المسار ضمن وحدات منفصلة أمرًا شائعًا، وتدعى في أغلب الأحيان "خدمات". ويعود أصل هذه التسمية إلى مفهوم التصميم المقاد بالمجال Domain driven design، ومن ثم جعله إطار العمل Spring أكثر شعبية.
لننشئ مجلدًا للخدمات يدعى src/services، ونضع الملف diaryService.ts فيه. يحتوي الملف على دالتين لإحضار وتخزين مُدخلات المذكرة:
import diaryData from '../../data/diaries.json' const getEntries = () => { return diaryData; }; const addEntry = () => { return null; }; export default { getEntries, addEntry };
لكن سنلاحظ خللًا ما:
يخبرنا المحرر أننا قد نحتاج إلى استعمال الإعداد resolveJsonModule في ملف التهيئة tsconfig. سنضيفه إذًا إلى قائمة الإعدادات:
{ "compilerOptions": { "target": "ES6", "outDir": "./build/", "module": "commonjs", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "esModuleInterop": true, "resolveJsonModule": true } }
وهكذا ستختفي المشكلة:
اقتباسملاحظة: لأسباب معينة قد تخبرك بيئة التطوير VSCode بأنها لا تستطيع إيجاد الملف data/diaries.json/../.. من الخدمة على الرغم من وجوده. هذه ثغرة في المحرر، ويختفي الخطأ بمجرد إعادة تشغيله.
وجدنا في وقت سابق كيف يمكن للمصرِّف أن يقرر نوع المتغير بناء على القيمة التي أسندت إليه. وبالمثل يمكن للمصرِّف تفسير مجموعات كبيرة من البيانات تتضمن كائنات ومصفوفات. وبناء على ذلك يمكن للمصرِّف أن يحذرنا عندما نحاول أن نفعل شيئًا قد يعتبره مريبًا باستخدام بيانات JSON التي نعالجها. فلو كنا نتعامل على سبيل المثال مع مصفوفة من كائنات محددة النوع، وحاولنا إضافة كائن لا يمتلك كل حقول كائنات المصفوفة أو خللًا في نوع الحقل (number بدل string مثلًا)، سيحذرنا المصرِّف مباشرة.
وعلى الرغم من فائدة المصرِّف في التحقق من أننا لم نفعل شيئًا خاطئًا، فتحديد أنواع البيانات بأنفسنا هو الخيار الأكثر أمانًا.
ما لدينا حاليًا، هو تطبيق express باستخدام TypeScript يعمل بشكل محدود، لكن لا وجود لأية أنواع في شيفرته. وطالما أننا على دراية بأنواع البيانات التي سنستقبلها في حقلي weather
، وvisibility
، فلا سبب سيدفعنا لإدراج نوعيهما في الشيفرة.
لننشئ ملفًا للأنواع يدعى types.ts، وسنعرّف ضمنه كل الأنواع التي سنستخدمها في المشروع.
أولًا سنعرف نوعين لقيم weather
، وvisibility
باستخدام نوع موّحد union type للقيم النصية التي يمكن أن يأخذها الحقلين:
export type Weather = 'sunny' | 'rainy' | 'cloudy' | 'windy' | 'stormy'; export type Visibility = 'great' | 'good' | 'ok' | 'poor';
سنتابع بإنشاء واجهة نوع interface باسم DiaryEntry:
export interface DiaryEntry { id: number; date: string; weather: Weather; visibility: Visibility; comment: string; }
وسنحاول تحديد نوع لبيانات JSON المُدرجة:
import diaryData from '../../data/diaries.json'; import { DiaryEntry } from '../types'; const diaries: Array<DiaryEntry> = diaryData; const getEntries = (): Array<DiaryEntry> => { return diaries;}; const addEntry = () => { return null; }; export default { getEntries, addEntry };
ولأننا صرحنا مسبقا عن القيم، فإسناد نوع لها سيوّلد خطأً:
تُظهر نهاية رسالة الخطأ المشكلة: الحقل غير ملائم. فقد حددنا نوعه في واجهة النوع على أنه DiaryEntry، لكن المصرِّف استدل من القيمة المسندة إليه أنه من النوع string.
يمكن حل المشكلة بتأكيد النوع type assertion. ولا ينبغي فعل ذلك إلا عندما نعلم قطعًا مالذي نفعله. فلو أكدنا أن نوع المتغير diaryData
هو DiaryEntry باستخدام التعليمة as
سيعمل كل شيء على مايرام.
import diaryData from '../../data/entries.json' import { Weather, Visibility, DiaryEntry } from '../types' const diaries: Array<DiaryEntry> = diaryData as Array<DiaryEntry>; const getEntries = (): Array<DiaryEntry> => { return diaries; } const addEntry = () => { return null } export default { getEntries, addEntry };
لا تستخدم أسلوب تأكيد النوع إلا في غياب أية طرق أخرى للمتابعة. فتأكيد نوع غير مناسب سيسبب أخطاءً مزعجة في زمن التشغيل. على الرغم من أن المصرِّف سيثق بك عندما تستخدم as
، لكن بعملك هذا لن تكون قد سخرت الإمكانيات الكاملة للغة TypeScript بل اعتمدت على المبرمج لضمان عمل الشيفرة.
من الممكن في حالتنا تغيير الطريقة التي نصدّر بها البيانات، وذلك بإعطائها نوعًا. وبما أننا لا نستطيع استخدام الأنواع داخل ملف JSON لابد من تحويله إلى ملف TS الذي يصدِّر البيانات على النحو التالي:
import { DiaryEntry } from "../src/types"; const diaryEntries: Array<DiaryEntry> = [ { "id": 1, "date": "2017-01-01", "weather": "rainy", "visibility": "poor", "comment": "Pretty scary flight, I'm glad I'm alive" }, // ... ]; export default diaryEntries;
وهكذا سيفسر المصرِّف المصفوفة عند إدراجها بشكل صحيح، وسيفهم نوع الحقلين weather
وvisibility
:
import diaries from '../../data/diaries'; import { DiaryEntry } from '../types'; const getEntries = (): Array<DiaryEntry> => { return diaries; } const addEntry = () => { return null; } export default { getEntries, addEntry };
لو أردت أن تُخزّن مُدخلًا ما باستثناء حقل محدد، يمكنك ضبط نوع هذا الحقل كحقل اختياري بإضافة "؟" إلى تعريف النوع:
export interface DiaryEntry { id: number; date: string; weather: Weather; visibility: Visibility; comment?: string; }
وحدات Node وJSON
من المهم أن تنتبه إلى مشكلة قد تظهر عند استخدام الخيار resolveJsonModule في الملف tsconfig:
{ "compilerOptions": { // ... "resolveJsonModule": true } }
فبناء على توثيق node المتعلق بملفات الوحدات، ستحاول node أن تنفّذ الوحدات وفق ترتيب اللاحقات كالتالي:
["js", "json", "node"]
وبالإضافة إلى ذلك ستوسِّع المكتبتان ts-node وts-node-dev قائمة اللاحقات لتصبح كالتالي:
["js", "json", "node", "ts", "tsx"]
اقتباسملاحظة: إن صلاحية استخدام ملفات باللاحقات js. و json. وnode على أساس وحدات في TypeScript، سيعتمد على إعدادات تهيئة بيئة التطوير بما في ذلك خيارات tsconfig، مثل: allowJs وresolveJsonModule.
لنتأمل الهيكيلة التالية لمجلد يحتوي على ملفات:
├── myModule.json └── myModule.ts
إن ضبطنا الخيار resolveJsonModule على القيمة true في TypeScript، سيصبح الملف myModule.json وحدة node صالحة للاستخدام. لنتخيل الآن السيناريو الذي نرغب فيه باستخدام الملف myModule.ts:
import myModule from "./myModule";
بالنظر إلى ترتيب لاحقات الوحدات في node:
["js", "json", "node", "ts", "tsx"]
سنلاحظ أن الأولوية ستكون للملف ذو اللاحقة "json." على الملف ذو اللاحقة "ts."، وبالتالي سيُدرج الملف myModule.json بدلًا من الملف myModule.ts.
ولكي نتجنب هذه الثغرة التي ستضيع الكثير من الوقت، يفضل أن نسمي الملفات التي تحمل لاحقة تفهمها node بأسماء مختلفة.
الأنواع الخدمية
قد نحتاج أحيانًا إلى إجراء تعديلات محددة على نوع. فلنفترض مثلًا وجود صفحة لعرض قائمة من البيانات التي يعتبر بعضها بيانات حساسة وأخرى غير حساسة. ونريد أن نضمن عدم عرض البيانات الحساسة. يمكننا اختيار الحقول التي نريدها من نوع محدد، عن طريق استخدام أحد الأنواع الخدمية Utility type ويدعى Pick.
ينبغي علينا في مشروعنا أن نفترض أن Ilari يريد إظهار قائمة بالمذكرات دون أن يُظهر حقل التعليقات، فقد يكتب شيئًا لايريد إظهاره لأحد، وخاصة في الرحلات التي أفزعته بشدة.
يسمح النوع الخدمي Pick أن نختار ما نريد استخدامه من حقول نوع محدد. ويستخدم هذا النوع لإنشاء نوع جديد تمامًا أو لإعلام الدالة ما يجب أن تعيده في زمن التشغيل. فالأنواع الخدمية هي صنف خاص من أدوات إنشاء الأنواع، لكن يمكن استخدامها كما نستخدم أي نوع آخر.
ولكي ننشئ نسخة خاضعة للرقابة من النوع DiaryEntry، سنستخدم Pick عند تعريف الدالة:
const getNonSensitiveEntries = (): Array<Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>> => { // ... }
سيتوقع المصرِّف أن تعيد الدالة مصفوفة قيم من النوع DiaryEntry المعدّل والذي يتضمن الحقول الأربعة المختارة فقط.
وطالما أن النوع Pick سيتطلب أن يكون النوع الذي سيعدّله معطًى كنوع متغير، كما هو حال المصفوفة، سيكون لدينا نوعين متغيرين ومتداخلين، وستبدو العبارة غريبة بعض الشيء. يمكن تحسين القدرة على قراءة الشيفرة باستخدام العبارة البديلة للمصفوفة:
const getNonSensitiveEntries = (): Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>[] => { // ... }
سنحتاج في حالتنا إلى استثناء حقل واحد، لذلك من الأفضل أن نستخدم النوع الخدمي Omit والذي يحدد الحقول التي يجب استبعادها من نوع:
const getNonSensitiveEntries = (): Omit<DiaryEntry, 'comment'>[] => { // ... }
وكطريقة أخرى، يمكن التصريح عن نوع جديد NonSensitiveDiaryEntry كالتالي:
export type NonSensitiveDiaryEntry = Omit<DiaryEntry, 'comment'>;
ستصبح الشيفرة الآن على النحو:
import diaries from '../../data/diaries'; import { NonSensitiveDiaryEntry, DiaryEntry } from '../types'; const getEntries = (): DiaryEntry[] => { return diaries; }; const getNonSensitiveEntries = (): NonSensitiveDiaryEntry[] => { return diaries; }; const addEntry = () => { return null; }; export default { getEntries, addEntry, getNonSensitiveEntries};
هنالك شيء واحد سيسبب القلق في تطبيقنا. ستعيد الدالة getNonSensitiveEntries
كامل حقول المُدخلات عند تنفيذها، ولن يعطينا المصرِّف أية أخطاء على الرغم من تحديد نوع المعطيات المعادة.
يحدث هذا لسبب بسيط هو أن TypeScript ستتحقق فقط من وجود كل الحقول المطلوبة، ولن تمنع وجود حقول زائدة. فلن تمنع في حالتنا إعادة كائن من النوع [ ]DiaryEntry، لكن لو حاولنا الوصول إلى الحقل comment
فلن نتمكن من ذلك، لأن TypeScript لا تراه على الرغم من وجوده.
سيقود ذلك إلى سلوك غير مرغوب إن لم تكن مدركًا ما تفعله. فلو كنا سنعيد كل المُدخلات عند تنفيذ الدالة getNonSensitiveEntries
إلى الواجهة الأمامية، ستكون النتيجة وصول قيمٍ لحقول لانريدها إلى المتصفح الذي أرسل الطلب، وهذا مخالف لتعريف نوع القيم المعادة.
علينا إذًا استثناء تلك الحقول بأنفسنا، طالما أن TypeScript لا تعدّل البيانات الفعلية المعادة بل نوعها فقط:
import diaries from '../../data/entries.js' import { NonSensitiveDiaryEntry, DiaryEntry } from '../types' const getEntries = () : DiaryEntry[] => { return diaries } const getNonSensitiveEntries = (): NonSensitiveDiaryEntry [] => { return diaries.map(({ id, date, weather, visibility }) => ({ id, date, weather, visibility, }));}; const addDiary = () => { return [] } export default { getEntries, getNonSensitiveEntries, addDiary }
فلو أردنا الآن إعادة هذه البيانات على أنها من النوع "DiaryEntry" كالتالي:
const getNonSensitiveEntries = () : DiaryEntry[] => {
سنحصل على الخطأ التالي:
سيقدم لنا السطر الأخير من الرسالة هذه المرة أيضًا فكرة الحل. لنتراجع عن هذا التعديل.
تتضمن الأنواع الخدمية العديد من الوسائل المفيدة، وتستحق أن نقف عندها لبعض الوقت ونطلع على توثيقها.
وأخيرًا يمكننا إكمال المسار الذي يعيد كل المُدخلات:
import express from 'express'; import diaryService from '../services/diaryService'; const router = express.Router(); router.get('/', (_req, res) => { res.send(diaryService.getNonSensitiveEntries());}); router.post('/', (_req, res) => { res.send('Saving a diary!'); }); export default router;
وستكون الاستجابة كما نريدها:
التمرينان 9.10 -9.11
لن نستخدم في هذه التمارين قاعدة بيانات حقيقية في تطبيقنا، بل بيانات محضرة مسبقًا موجودة في الملفين diagnoses.json وpatients.json. نزِّل الملفين وخزنهما في مجلد يدعى data في مشروعك. ستجري التعديلات على البيانات في ذاكرة التشغيل فقط، فلا حاجة للكتابة على الملفين في هذا القسم.
9.10 واجهة خلفية لتطبيق إدارة المرضى: الخطوة 3
أنشئ النوع daiagnoses واستخدمه لإنشاء وصلة التخديم لإحضار كل تشخيصات المريض من خلال الطلب HTTP -GET. اختر هيكيلة مناسبة لشيفرتك عبر اختيار أسماء ملائمة للملفات والمجلدات.
ملاحظة: قد يحتوي النوع daiagnoses على الحقل latin
وقد لا يحتويه. ربما ستحتاج إلى استخدام الخصائص الاختيارية عند تعريف النوع.
9.11 واجهة خلفية لتطبيق إدارة المرضى: الخطوة 4
أنشئ نوعًا للبيانات باسم Patient ثم أنشئ وصلة تخديم لطلبات GET على العنوان api/patients/ لتعيد بيانات كل المرضى إلى الواجهة الأمامية لكن دون الحقل ssn
. استخدم الأنواع الخدمية للتأكد من أنك أعدت الحقول المطلوبة فقط.
يمكنك في هذا التمرين افتراض نوع الحقل gender
على أنه string.
جرب وصلة التخديم من خلال المتصفح، وتأكد أن الحقل ssn
غير موجود في بيانات الاستجابة:
تأكد من عرض الواجهة الأمامية لقائمة المرضى بعد إنشائك لوصلة التخديم:
منع الحصول على نتيجة غير محددة عرضيا
سنوسع الواجهة الخلفية لدعم وظيفة الحصول على مُدخل محدد من خلال الطلبGET إلى العنوان api/diaries/:id.
سنوسّع الخدمة DiaryService بإنشاء الدالة findById
:
// ... const findById = (id: number): DiaryEntry => { const entry = diaries.find(d => d.id === id); return entry;}; export default { getEntries, getNonSensitiveEntries, addDiary, findById}
مرة أخرى سيواجهنا الخطأ التالي:
تظهر المشكلة لأن وجود مُدخل بمعرِّف id محدد غير مضمون. من الجيد أننا أدركنا هذه المشكلة في مرحلة التصريف. فلو لم نستخدم TypeScript لما جرى تحذيرنا من قبل المصرِّف، وقد نعيد في السيناريو الأسوء كائنًا غير محدد بدلًا من تنبيه المستخدم أنه لاتوجد مُدخلات بهذا المعرِّف.
نحتاج قبل كل شيء في حالات كهذه إلى اتخاذ قرار بشأن القيمة المعادة إن لم نجد الكائن المطلوب، وكيفية معالجة هذه الحالة. يعيد تابع المصفوفات find
كائنًا غير محدد إن لم يجد الكائن المطلوب، ولا مشكلة في ذلك. إذ يمكن حل مشكلتنا بتحدبد نوع للقيمة المعادة على النحو التالي:
const findById = (id: number): DiaryEntry | undefined => { const entry = diaries.find(d => d.id === id); return entry; }
وسنستخدم معالج المسار التالي:
import express from 'express'; import diaryService from '../services/diaryService' router.get('/:id', (req, res) => { const diary = diaryService.findById(Number(req.params.id)); if (diary) { res.send(diary); } else { res.sendStatus(404); } }) // ... export default router;
إضافة مذكرة جديدة
لنبدأ بإنشاء وصلة تخديم لطلبات HTTP-POST وذلك لإضافة مّدخلات جديدة إلى المذكرات. ينبغي أن تحمل المدخلات النوع ذاته للبيانات المطلوبة.
تعالج الشيفرة التالية بيانات الاستجابة:
router.post('/', (req, res) => { const { date, weather, visibility, comment } = req.body; const newDiaryEntry = diaryService.addDiary( date, weather, visibility, comment, ); res.json(newDiaryEntry); });
وسيكون التابع الموافق في شيفرة الخدمة DiaryService كالتالي:
import { NonSensitiveDiaryEntry, DiaryEntry, Visibility, Weather} from '../types'; const addDiary = ( date: string, weather: Weather, visibility: Visibility, comment: string ): DiaryEntry => { const newDiaryEntry = { id: Math.max(...diaries.map(d => d.id)) + 1, date, weather, visibility, comment, } diaries.push(newDiaryEntry); return newDiaryEntry; };
وكما ترى، سيغدو من الصعب قراءة الدالة addDiary
نظرًا لكتابة كل الحقول كمعاملات منفصلة. وقد يكون من الأفضل لو أرسلنا البيانات ضمن كائن واحد إلى الدالة:
router.post('/', (req, res) => { const { date, weather, visibility, comment } = req.body; const newDiaryEntry = diaryService.addDiary({ date, weather, visibility, comment, }); res.json(newDiaryEntry); })
لكن ما نوع هذا الكائن؟ فهو ليس من النوع DiaryEntry تمامًا كونه يفتقد الحقل id
. من الأفضل هنا لو أنشأنا نوعًا جديدًا باسم NewDiaryEntry للمُدخلات التي لم تُخزَّن بعد. لنعرّف هذا النوع في الملف types.ts مستخدمين النوع DiaryEntry مع النوع الخدمي Omit.
export type NewDiaryEntry = Omit<DiaryEntry, 'id'>;
يمكننا الآن استخدام النوع الجديد في الخدمة DiaryService، بحيث يُفكَّك الكائن الجديد عند إنشاء مُدخل جديد من أجل تخزينه:
import { NewDiaryEntry, NonSensitiveDiaryEntry, DiaryEntry } from '../types'; // ... const addDiary = ( entry: NewDiaryEntry ): DiaryEntry => { const newDiaryEntry = { id: Math.max(...diaries.map(d => d.id)) + 1, ...entry }; diaries.push(newDiaryEntry); return newDiaryEntry; };
ستبدو الشيفرة أوضح الآن!
ولتفسير البيانات المستقبلة، علينا تهيئة أداة JSON الوسطية:
import express from 'express'; import diaryRouter from './routes/diaries'; const app = express(); app.use(express.json()); const PORT = 3000; app.use('/api/diaries', diaryRouter); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
أصبح التطبيق الآن جاهزًا لاستقبال طلبات HTTP-POST لإنشاء مُدخلات جديدة بالنوع الصحيح.
التأكد من الطلب
هناك الكثير من الأمور التي قد تسير بشكل خاطئ عند استقبال بيانات من مصدر خارجي. فلا يمكن أن تعمل التطبيقات بنفسها إلا نادرًا، وعلينا أن نقبل حقيقة أن البيانات التي نحصل عليها من خارج منظومة التطبيق غير موثوقة بشكل كامل. فلا يمكن أن تكون البيانات عند استقبالها من مصدر خارجي محددة النوع مسبقًا. وعلينا أن نقرر الأسلوب الذي سنتعامل به مع ضبابية هذه الحالة.
تعالج express الموضوع بتأكيد النوع any لكل حقول جسم الطلب. لن يكون هذا السلوك واضحًا للمصرِّف في حالتنا، لكن لو حاولنا إلقاء نظرة أقرب إلى المتغيرات، ومررنا مؤشر الفأرة فوق كل منها، سنجد أن نوع كل منها هو بالفعل any. وبالتالي لن يعترض المحرر أبدًا عندما نمرر هذه البيانات إلى الدالة addDiary
كمعاملات.
يمكن إسناد القيم من النوع any إلى متغيرات من أي نوع، فلربما تكون هي القيمة المطلوبة. وهذا الأسلوب يفتقر بالتأكيد إلى عامل الأمان، لذا علينا التحقق من القيم المستقبلة سواءً استخدمنا TypeScript أو لا.
بالإمكان إضافة آليتي تحقق بسيطتين exist
و is_value_valid
ضمن الدالة التي تعرّف الوجهة route، لكن من الأفضل كتابة منطق تفسير البيانات وتقييمها في ملف منفصل "utils.ts".
لابد من تعريف الدالة toNewDiaryEntry
التي تستقبل جسم الطلب كمعامل وتعيد كائن NewDiaryEntry
بنوع مناسب. يستخدم تعريف الوجهة هذه الدالة على النحو التالي:
import toNewDiaryEntry from '../utils'; // ... router.post('/', (req, res) => { try { const newDiaryEntry = toNewDiaryEntry(req.body); const addedEntry = diaryService.addDiary(newDiaryEntry); res.json(addedEntry); } catch (e) { res.status(400).send(e.message); } })
طالما أننا نحاول الآن كتابة شيفرة آمنة، وأن نتأكد من تلقي البيانات التي نريدها تمامًا من الطلبات، علينا أن نتحقق من ترجمة وتقييم بيانات كل حقل نتوقع أن نستقبله.
سيبدو هيكل الدالة toNewDiaryEntry
على النحو التالي:
import { NewDiaryEntry } from './types'; const toNewDiaryEntry = (object): NewDiaryEntry => { const newEntry: NewDiaryEntry = { // ... } return newEntry; } export default toNewDiaryEntry;
على الدالة ترجمة كل حقل والتأكد أن نوع كل حقل من حقول القيمة المعادة هو NewDiaryEntry. ويعني هذا التحقق من كل حقل على حدى.
ستواجهنا أيضًا في هذه المرحلة مشاكل تتعلق بالأنواع: ماهو نوع الكائن المعاد، طالما أنه جسم الطلب في الحقيقة من النوع any؟ إن فكرة هذه الدالة، هي الربط بين الحقول مجهولة النوع بحقول من النوع الصحيح والتحقق من أنها معرّفة كحقول متوقعة أولا. وربما تكون هذه الحالة هي الحالة النادرة التي سنسمح فيها باستخدام النوع any.
سيعترض المدقق eslint على استخدام النوع any:
وسبب ذلك، هو تفعيل القاعدة no-explicit-any التي تمنع التصريح علنًا عن النوع any. وهي في الواقع قاعدة جيدة لكنها غير مطلوبة في هذا الجزء من الملف. يمككنا أن نتجاوز هذه القاعدة بإلغاء تفعيلها عن طريق وضع السطر التالي في ملف الشيفرة:
/* eslint-disable @typescript-eslint/no-explicit-any */
سنبدأ الآن بإنشاء المفسّرات لكل حقل من حقول الكائن المُعاد.
لتقييم الحقل comment
لابد من التحقق أنه موجود وأنه يحمل النوع string.
ستبدو الدالة على نحو مماثل لما يلي:
const parseComment = (comment: any): string => { if (!comment || !isString(comment)) { throw new Error('Incorrect or missing comment: ' + comment); } return comment; }
تتلقى الدالة معاملُا من النوع any وتعيده من النوع string إن كان موجودًا ومن النوع الصحيح.
ستبدو دالة تقييم النوع string كالتالي:
const isString = (text: any): text is string => { return typeof text === 'string' || text instanceof String; };
تدعى هذه الدالة أيضًا بالدالة حامية النوع type guard. وتعرَّف بأنها دالة تعيد قيمة منطقية ونوعًا إسناديُا (type predicate -يسند نوعا صريحًا لقيمة معادة-). سيكون شكل النوع الإسنادي في حالتنا هو التالي:
text is string
إن الشكل العام للنوع الإسنادي هو parameterName is Type
حيث يمثل parameterName معامل الدالة ويمثل "Type" النوع الذي سيُعاد.
فإن أعادت الدالة حامية النوع القيمة true، سيعرف مصرِّف TypeScript أن للمتغير المُختَبَر النوع ذاته الذي حدده النوع الإسنادي.
قبل أن تُستدعى الدالة حامية للنوع سيكون النوع الفعلي للمتغير comment
مجهولًا.
لكن بعد استدعائها سيعلم المصرِّف -في حال أعادت حامية النوع القيمة true، أن المتغير من النوع string.
لماذا نرى شرطين في حامية النوع string؟
const isString = (text: any): text is string => { return typeof text === 'string' || text instanceof String;}
أليس كافيًا أن نكتب شيفرة الحامية على الشكل:
const isString = (text: any): text is string => { return typeof text === 'string'; }
إن الشكل الأبسط في أغلب الأحيان كافٍ للتطبيق العملي. لكن إن أردنا أن نكون متأكدين تمامًا لابد من وجود الشرطين. فهنالك طريقتان لإنشاء كائن من النوع string في JavaScript وكلاهما يعمل بطريقة تختلف قليلًا عن الآخر بما يتعلق باستخدام العاملين typeof
و instanceof
.
const a = "I'm a string primitive"; const b = new String("I'm a String Object"); typeof a; --> returns 'string' typeof b; --> returns 'object' a instanceof String; --> returns false b instanceof String; --> returns true
لن يفكر في الغالب أحد باستخدام الدالة البانية لإنشاء قيمة نصية، وبالتالي ستكون النسخة الأبسط للدالة الحامية مناسبة.
لنتأمل الآن الحقل date
. سيُترجم ويُقيَّم هذا الحقل بطريقة مشابه كثيرًا لطريقة تقييم الحقل comment
. وطالما أن TypeScript لا تعرف نوعًا للتاريخ، سنضطر للتعامل معه كنص string. ولابد كذلك من التحقق على مستوى JavaScript أن تنسيق التاريخ مقبول.
سنضيف الدوال التالية:
const isDate = (date: string): boolean => { return Boolean(Date.parse(date)); }; const parseDate = (date: any): string => { if (!date || !isString(date) || !isDate(date)) { throw new Error('Incorrect or missing date: ' + date); } return date; };
لا يوجد شيء مميز في هذه الشيفرة، لكن يجدر الإشارة أنه لا يمكن استعمال الدالة حامية النوع لأن التاريخ هنا سيعتبر من النوع string ليس إلا. ولاحظ على الرغم من أن الدالة parseDate
تقبل المتغير date
على أنه من النوع any، فسيتغير نوعه إلى string بعد التحقق من نوعه باستخدام isString
. وهذا مايفسر الحاجة لقيمة نصية صحيحة التنسيق، عند تمرير المتغير إلى الدالة isDate
.
سننتقل الآن إلى النوعين الأخيرين Weather و Visibility.
نريد أن تجري عمليتي الترجمة والتقييم كالتالي:
const parseWeather = (weather: any): Weather => { if (!weather || !isString(weather) || !isWeather(weather)) { throw new Error('Incorrect or missing weather: ' + weather) } return weather; };
لكن كيف يمكننا تقييم نص ما على أنه من تنسيق أو شكل محدد؟ تقتضي أحدى الطرق المتبعة، كتابة دالة حامية للنوع بالشكل التالي:
const isWeather = (str: string): str is Weather => { return ['sunny', 'rainy', 'cloudy', 'stormy' ].includes(str); };
سيعمل الحل السابق بشكل جيد، لكن المشكلة أن قائمة الأحوال الجوية المحتملة قد لاتبقى متزامنة مع تعريفات النوع إن حدث أي تبديل فيها (خطأ في الكتابة مثلًا). وهذا بالطبع ليس جيدًا، لأننا نرغب بوجود مصدر وحيد لكل أنواع الطقس الممكنة.
من الأفضل في حالتنا أن نحسَّن تعريف أنواع الطقس. فبدلًا من النوع البديل لابد من استخدام تعداد TypeScript الذي يسمح لنا باستخدام القيمة الفعلية في الشيفرة في زمن التشغيل، وليس فقط في مرحلة التصريف.
لنعد تعريف النوع Weather ليصبح كالتالي:
export enum Weather { Sunny = 'sunny', Rainy = 'rainy', Cloudy = 'cloudy', Stormy = 'stormy', Windy = 'windy', }
يمكننا التحقق الآن أن القيمة النصية المستقبلة هي إحدى أنواع الطقس الممكنة، وستكتب الدالة الحامية للنوع كالتالي:
const isWeather = (param: any): param is Weather => { return Object.values(Weather).includes(param); };
لاحظ كيف غيرنا نوع المعامل إلى any فلو كان من النوع string، لن يُترجَم التابع includes
. وهذا منطقي وخاصة إذا أخذنا في الحسبان إعادة استخدام الدالة لاحقًا. بهذه الطريقة سنكون متأكدين تمامًا أنه أيًا كانت القيمة التي ستمرر إلى الدالة، فستخبرنا إن كانت القيمة مناسبة للطقس أو لا.
يمكن تبسيط الدالة قليلًا كما يلي:
const parseWeather = (weather: any): Weather => { if (!weather || !isWeather(weather)) { throw new Error('Incorrect or missing weather: ' + weather); } return weather; };
ستظهر مشكلة بعد هذه التغييرات. لن تلائم البيانات الأنواع التي عرّفناه بعد الآن.
والسبب أننا لا نستطيع ببساطة افتراض أن القيمة string على أنها تعداد enum.
يمكننا حل المشكلة بربط عناصر البيانات الأولية (المحضرة مسبقًا) بالنوع DiaryEntry مستخدمين الدالة toNewDiaryEntry
:
import { DiaryEntry } from "../src/types"; import toNewDiaryEntry from "../src/utils"; const data = [ { "id": 1, "date": "2017-01-01", "weather": "rainy", "visibility": "poor", "comment": "Pretty scary flight, I'm glad I'm alive" }, // ... ] const diaryEntries: DiaryEntry [] = data.map(obj => { const object = toNewDiaryEntry(obj) as DiaryEntry object.id = obj.id return object }) export default diaryEntries
وطالما أن الدالة ستعيد كائنًا من النوع NewDiaryEntry، فعلينا التأكيد على أنه من النوع DiaryEntry باستخدام العامل as.
يستخدم التعداد عادة عندما نكون أمام مجموعة من القيم المحددة مسبقًا ولا نتوقع تغييرها في المستقبل، خاصة القيم التي يصعب تغييرها مثل أيام الأسبوع وأسماء الأشهر وغيرها. لكن طالما أنها تقدم أسلوبًا جيدًا في تقييم القيم الواردة فمن المفيد استخدامها في حالتنا.
وكذلك الأمر لابد من التعامل مع النوع visibility بنفس الأسلوب. سيبدو التعداد الخاص به كالتالي:
export enum Visibility { Great = 'great', Good = 'good', Ok = 'ok', Poor = 'poor', }
وستمثل الشيفرة التالية الدالة الحامية للنوع:
const isVisibility = (param: any): param is Visibility => { return Object.values(Visibility).includes(param); }; const parseVisibility = (visibility: any): Visibility => { if (!visibility || !isVisibility(visibility)) { throw new Error('Incorrect or missing visibility: ' + visibility); } return visibility; };
وهكذا نكون قد انتهينا من إنشاء الدالة toNewDiaryEntry
التي تتحمل مسؤولية تقييم وتفسير حقول بيانات الطلب:
const toNewDiaryEntry = (object: any): NewDiaryEntry => { return { date: parseDate(object.date), comment: parseComment(object.comment), weather: parseWeather(object.weather), visibility: parseVisibility(object.visibility) }; };
وبهذا ننهي النسخة الأولى من تطبيق مذكرات الرحلات الجوية!
وفي حال أردنا إدخال مذكرة جديدة بحقول مفقودة أو غير صالحة سنحصل على رسالة الخطأ التالية:
التمرينان 9.12 - 9.13
9.12 واجهة خلفية لتطبيق إدارة المرضى: الخطوة 5
أنشأ وصلة تخديم للطلب POST على العنوان api/patients/ بغرض إضافة مرضى جدد. وتأكد من إمكانية إضافة مرضى عبر الواجهة الأمامية أيضًا.
9.13 واجهة خلفية لتطبيق إدارة المرضى: الخطوة 6
جد آلية أمنة لتفسير وتقييم وحماية الأنواع لطلبات HTTP-POST إلى العنوان api/patients/
أعد كتابة الحقل Gender
مستخدمًا التعداد (enum).
تصريف -وبتصرف- للفصل Typing the express app من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.