إضافة TypeScript إلى @ wordpress / data Stores
نشرت: 2021-02-05تحدثنا العام الماضي كثيرًا عن TypeScript. في إحدى مشاركاتي الأخيرة ، رأينا كيفية استخدام TypeScript في مكونات WordPress الإضافية من خلال مثال حقيقي ، وعلى وجه الخصوص ، كيفية تحسين متجر Redux عن طريق إضافة أنواع إلى محدداتنا وإجراءاتنا ومخفضاتنا.
في المثال المذكور ، انتقلنا من كود JavaScript أساسي مثل هذا:
// Selectors function getPost( state, id ) { … } function getPostsInDay( state, day ) { … } // Actions function receiveNewPost( post ) { … } function updatePost( postId, attributes ) { … } // Reducer function reducer( state, action ) { … }حيث أن الشيء الوحيد الذي أعطانا أدلة حول ما تفعله كل وظيفة وما هي كل معلمة يعتمد على قدرات التسمية لدينا ، إلى نظير TypeScript المحسن التالي:
// Selectors function getPost( state: State, id: PostId ): Post | undefined { … } function getPostsInDay( state: State, day: Day ): PostId[] { … } // Actions function receiveNewPost( post: Post ): ReceiveNewPostAction { … } function updatePost( postId: PostId, attributes: Partial<Post> ): UpdatePostAction { … } // Reducer function reducer( state: State, action: Action ): State { … }مما يجعل كل شيء أكثر وضوحًا ، حيث يتم كتابة كل شيء بشكل صحيح:
type PostId = number; type Day = string; type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Dictionary; days: Dictionary; };قبل أسبوعين كنت أعمل على المكون الإضافي الجديد ، Nelio Unlocker ، وواجهت مشكلة عند تطبيق كل هذه التقنيات. لذلك دعونا نراجع المشكلة المذكورة ونتعلم كيفية التغلب عليها!
المشكلة
كما تعلم بالفعل ، عندما نريد استخدام المحددات و / أو الإجراءات التي حددناها في متجرنا ، فإننا نفعل ذلك من خلال الوصول إليها من خلال خطافات React (مع useSelect and useDispatch ) أو عبر مكونات ذات ترتيب أعلى (باستخدام withDispatch withSelect ، يتم توفيرها جميعًا بواسطة حزمة @wordpress/data .
على سبيل المثال ، إذا أردنا استخدام مُحدِّد getPost والإجراء updatePost الذي رأيناه للتو ، فكل ما علينا فعله هو شيء من هذا القبيل (بافتراض تسمية nelio-store ):
const Component = ( { postId } ): JSX.Element => { const post = useSelect( ( select ): Post => select( 'nelio-store' ).getPost( postId ); ); const { updatePost } = useDispatch( 'nelio-store' ); return ( ... ); };في المقتطف السابق ، يمكنك أن ترى أننا نصل إلى المحددات والإجراءات الخاصة بنا باستخدام خطافات React. ولكن كيف تعرف TypeScript بوجود هذه المحددات والإجراءات ، ناهيك عن أنواعها؟
حسنًا ، هذه بالضبط هي المشكلة التي واجهتها. هذا يعني أنني أردت أن أعرف كيف يمكنني إخبار TypeScript أن نتيجة الوصول إلى select('nelio-store') هي كائن يحتوي على جميع محددات المتجر dispatch('nelio-store') هو كائن مع إجراءات متجرنا .
الحل
في آخر مشاركة لنا على TypeScript تحدثنا عن الوظائف متعددة الأشكال. تتيح لنا الوظائف متعددة الأشكال تحديد أنواع إرجاع مختلفة بناءً على الحجج المعطاة. حسنًا ، باستخدام تعدد الأشكال من TypeScript ، يمكننا تحديد ذلك ، عندما نسمي طرق select أو dispatch لحزمة @wordpress/data مع اسم متجرنا كمعامل ، فإن النتيجة التي نحصل عليها هي محدداتنا وأفعالنا على التوالي.
للقيام بذلك ، ما عليك سوى إضافة declare module في الملف حيث نسجل متجرنا على النحو التالي:
// WordPress dependencies import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; // Internal dependencies import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; const STORE = 'nelio-store'; registerStore( STORE, { controls, reducer, actions, selectors, } ); // Extend @wordpress/data with our store declare module '@wordpress/data' { function select( key: typeof STORE ): Selectors; function dispatch( key: typeof STORE ): Actions; } ثم حدد ما هي أنواع Selectors Actions في الواقع:
type Selectors = { getPost: ( id: PostId ) => Post | undefined; getPostsInDay: ( day: Day ) => PostId[]; } type Actions = { receiveNewPost: ( post: Post ) => void; updatePost: ( postId: PostId, attributes: Partial<Post> ) => void; } حتى الآن ، جيد جدًا ، أليس كذلك؟ المشكلة الوحيدة هي أنه يتعين علينا تحديد أنواع المحددات Actions يدويًا ، الأمر الذي يبدو غريبًا نظرًا لأن Selectors يعرف بالفعل أن لدينا مجموعة من selectors actions المكتوبة بشكل صحيح ...
معالجة أنواع الوظائف في TypeScript
إذا ألقينا نظرة على أنواع actions وكائنات selectors التي قمنا باستيرادها ، فسنرى أن TypeScript يخبرنا بما يلي:
typeof selectors === { getPost: ( state: State, id: PostId ) => Post | undefined; getPostsInDay: ( state: State, day: Day ) => PostId[]; } typeof actions === { receiveNewPost: ( post: Post ) => ReceiveNewPostAction; updatePost: ( postId: PostId, attributes: Partial<Post> ) => UpdatePostAction; } كما ترى ، أنواعها هي نسخة طبق الأصل من الأنواع التي حددناها يدويًا في القسم السابق. حسنًا ، دقيق تقريبًا: المحددات تفتقد الوسيطة الأولى ( state المتجر ، لأنها غير موجودة عندما نسمي محددًا من select ) وتعيد الإجراءات void (حيث تُستدعى الإجراءات عبر dispatch لا تُرجع شيئًا).
هل يمكننا استخدامها لإنشاء أنواع Selectors Actions التي نحتاجها تلقائيًا؟
كيفية إزالة المعامل الأول لنوع دالة في TypeScript
دعونا نركز لحظة على محدد getPost . نوعه كالتالي:
// Old type typeof getPost === ( state: State, id: PostId ) => Post | undefined كما قلنا للتو ، نحتاج إلى نوع وظيفة جديد لا يحتوي على معلمة state :
// New type ( id: PostId ) => Post | undefinedلذلك ، نحتاج إلى TypeScript لإنشاء نوع جديد من نوع موجود بالفعل. يمكن تحقيق ذلك من خلال الجمع بين العديد من الوظائف المتقدمة للغة:
type OmitFirstArg< F > = F extends ( x: any, ...args: infer P ) => infer R ? ( ...args: P ) => R : never;معقدة ، أليس كذلك؟ دعنا نلقي نظرة فاحصة على ما يحدث هنا:
-
type OmitFirstArg<F>. بادئ ذي بدء ، نحدد نوعًا عامًا مساعدًا جديدًا (OmitFirstArg). بشكل عام ، النوع العام هو نوع يتيح لنا تحديد أنواع جديدة من الأنواع الموجودة بالفعل. على سبيل المثال ، ربما تكون معتادًا على نوعArray<T>، حيث يتيح لك إنشاء قوائم بالأشياء:Array<string>هي قائمة من السلاسل ، وArray<Post>هي قائمةPost، وما إلى ذلك. حسنًا ، ما يلي هذه الفكرة ،OmitFirstArg<F>هو نوع مساعد يزيل الوسيطة الأولى للدالة. - نظرًا لأن هذا نوع عام ، فيمكننا نظريًا استخدامه مع أي نوع TypeScript آخر. وهذا يعني أن أشياء مثل
OmitFirstArg<string>وOmitFirstArg<Post>ممكنة ... على الرغم من أننا نعلم أن هذا النوع يجب استخدامه فقط مع الوظائف التي تحتوي على وسيطة واحدة على الأقل. للتأكد من استخدام هذا النوع المساعد مع الوظائف فقط سنعرفه كنوع شرطي. يسمح النوع الشرطي بتحديد النوع الناتج الذي يجب أن يعتمد على شرط: "إذا كانتFدالة ذات وسيطة واحدة (شرط) على الأقل ، فإن النوع الناتج هو وظيفة أخرى حيث تمت إزالة الوسيطة الأولى (اكتب عندما يكون الشرط صحيح)؛ وإلا ، فاستخدمnevertype (النوع عندما يكون الشرط خطأ). " -
F extends XXX. هذه هي الصيغة لتحديد الشرط. هل تريد التحقق من أنFعبارة عن سلسلة؟ فقط اكتب:F extends string. سهل جدا. ولكن ماذا عن "دالة ذات حجة واحدة؟" من المؤكد أن هذا يبدو أكثر تعقيدًا ... -
(x: any, ...args: infer P) => infer Rهذا نوع دالة: نبدأ بالوسيطات (بين قوسين) ، متبوعة بسهم ، متبوعًا بنوع إرجاع الوظيفة. في هذه الحالة بالذات ، نطلب أن يكون للدالة وسيطة واحدةx(نوعها المحدد غير ذي صلة). يحتوي تعريف النوع هذا على بتتين مثيرتين للاهتمام. من ناحية أخرى ، نستخدم عامل التشغيل الباقي لالتقاط الأنواعPمنargsالمتبقية (إن وجدت). من ناحية أخرى ، نستخدم الاستدلال بنوع TypeScript (infer) لمعرفة ما هي هذه الأنواعPحقًا ، بالإضافة إلى نوع الإرجاع الدقيقR -
? (...args: P) => R : never? (...args: P) => R : never. أخيرًا ، نكمل النوع الشرطي. إذا كانتFدالة ، فإن نوع الإرجاع هو وظيفة جديدة تكون وسيطاتها من النوعPونوع الإرجاع الخاص بهاRإذا لم يكن كذلك ، فلن يكون نوع الإرجاعnever.
هذه هي الطريقة التي يمكننا بها استخدام هذا النوع المساعد لإنشاء النوع الجديد الذي أردناه:

const getPost = ( state: State, id: PostId ) => Post | undefined; OmitFirstArg< typeof getPost > === ( id: PostId ) => Post | undefined;ونحن بالفعل نقترب خطوة واحدة من تحقيق ما نريد! هنا يمكنك رؤية هذا المثال في الملعب.
كيفية تغيير نوع الإرجاع لنوع دالة في TypeScript
أنا متأكد من أنك تعرف الإجابة بالفعل من خلال معرفة: نحتاج إلى نوع عام مساعد يأخذ نوع دالة ويعيد نوع دالة جديدًا. شيء من هذا القبيل:
type RemoveReturnType< F > = F extends ( ...args: infer P ) => any ? ( ...args: P ) => void : never; حق سهل؟ إنه مشابه إلى حد كبير لما فعلناه في القسم السابق: نحن نلتقط أنواع args في P (ليست هناك حاجة لطلب وسيطة واحدة على الأقل x هذه المرة) ونتجاهل نوع الإرجاع. إذا كانت F دالة ، فقم بإرجاع دالة جديدة تُرجع void . خلاف ذلك ، لا تعود never . مدهش!
تحقق من هذا في الملعب.
كيفية تعيين نوع كائن إلى نوع كائن آخر في TypeScript
أفعالنا ومحدداتنا هما كائنان مفاتيحهما هي أسماء تلك الإجراءات والمحددات وقيمها هي الوظائف نفسها. هذا يعني أن أنواع هذه الكائنات تبدو كما يلي:
typeof selectors === { getPost: ( state: State, id: PostId ) => Post | undefined; getPostsInDay: ( state: State, day: Day ) => PostId[]; } typeof actions === { receiveNewPost: ( post: Post ) => ReceiveNewPostAction; updatePost: ( postId: PostId, attributes: Partial<Post> ) => UpdatePostAction; }في القسمين السابقين تعلمنا كيفية تحويل نوع واحد من الوظائف إلى نوع آخر من الوظائف. هذا يعني أنه يمكننا تحديد أنواع جديدة يدويًا مثل هذا:
type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; }; لكن ، بالطبع ، هذا لا يمكن أن يستمر بمرور الوقت: نحن نحدد يدويًا أسماء الوظائف في كلا النوعين. من الواضح أننا نريد تعيين تعريفات الأنواع الأصلية actions selectors تلقائيًا لأنواع جديدة.
إليك كيفية القيام بذلك في TypeScript:
type OmitFirstArgs< O > = { [ K in keyof O ]: OmitFirstArg< O[ K ] >; } type RemoveReturnTypes< O > = { [ K in keyof O ]: RemoveReturnType< O[ K ] >; }نأمل أن يكون هذا منطقيًا بالفعل ، لكن دعنا نلغي سريعًا ما يفعله المقتطف السابق على أي حال:
-
type OmitFirstArgs<O>. نقوم بإنشاء نوع عام مساعد جديد يأخذ كائنO - والنتيجة هي نوع كائن آخر (كما تكشف الأقواس المتعرجة
{...}). -
[K in keyof O]. لا نعرف بالضبط المفاتيح التي سيحتوي عليها الكائن الجديد ، لكننا نعلم أنه يجب أن تكون نفس المفاتيح لتلك المضمنة فيOهذا ما نقول لـ TypeScript: نريد جميع المفاتيحKالتي هيkeyof O - وبعد ذلك ، لكل مفتاح
K، نوعه هوOmitFirstArg<O[K]>. أي أننا نحصل على النوع الأصلي (O[K]) ونحوله إلى النوع الذي نريده باستخدام النوع الإضافي الذي حددناه (في هذه الحالة ،OmitFirstArg). - أخيرًا ، نفعل الشيء نفسه مع
RemoveReturnTypesوالنوع المساعد الأصليRemoveReturnType.
تمديد @wordpress/data باستخدام محدداتنا وإجراءاتنا
إذا أضفت الأنواع الأربعة المساعدة التي رأيناها اليوم في ملف global.d.ts في جذر مشروعك ، فيمكنك أخيرًا دمج كل ما رأيناه في هذا المنشور لحل المشكلة الأصلية:
// WordPress dependencies import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; // Internal dependencies import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; // Types type Selectors = OmitFirstArgs< typeof selectors >; type Actions = RemoveReturnTypes< typeof actions >; const STORE = 'nelio-store'; registerStore( STORE, { controls, reducer, actions, selectors, } ); // Extend @wordpress/data with our store declare module '@wordpress/data' { function select( key: typeof STORE ): Selectors; function dispatch( key: typeof STORE ): Actions; }وهذا كل شيء! أتمنى أن تكون قد أحببت نصيحة التطوير هذه ، وإذا كنت قد فعلت ذلك ، فيرجى مشاركتها مع زملائك وأصدقائك. أوه! وإذا كنت تعرف نهجًا مختلفًا للحصول على نفس النتيجة ، فأخبرني في التعليقات.
صورة مميزة بواسطة Gabriel Crismariu على Unsplash.
