การเพิ่ม 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 hooks (ด้วย useSelect และ useDispatch ) หรือผ่านส่วนประกอบที่มีลำดับสูงกว่า (ด้วย withSelect และ withDispatch ) ซึ่งทั้งหมดนี้มีให้โดยแพ็คเกจ @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 hooks แต่ TypeScript รู้ได้อย่างไรว่าตัวเลือกและการกระทำเหล่านั้นมีอยู่จริง นับประสาว่ามันคืออะไร?

นั่นคือปัญหาที่ฉันเผชิญ นั่นคือ ฉันต้องการทราบว่าฉันจะบอก TypeScript ได้อย่างไรว่าผลลัพธ์ของการเข้าถึง select('nelio-store') เป็นออบเจกต์ที่มีตัวเลือกร้านค้าทั้งหมดของเรา และ dispatch('nelio-store') เป็นออบเจ็กต์ที่มีการดำเนินการกับร้านค้าของเรา .

การแก้ไขปัญหา

ในโพสต์ล่าสุดของเราใน TypeScript เราได้พูดถึงฟังก์ชัน polymorphic ฟังก์ชัน Polymorphic ช่วยให้เราระบุประเภทการส่งคืนที่แตกต่างกันตามอาร์กิวเมนต์ที่กำหนด ด้วยการใช้พหุสัณฐานของ 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; }

จนถึงตอนนี้ดีมากใช่มั้ย? "ปัญหา" เพียงอย่างเดียวคือเราต้องกำหนดประเภท Selectors และการ Actions การด้วยตนเอง ซึ่งฟังดูแปลกเพราะ TypeScript รู้อยู่แล้วว่าเรามีชุด 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 เป็นฟังก์ชันที่มีอาร์กิวเมนต์อย่างน้อยหนึ่งตัว (เงื่อนไข) ประเภทผลลัพธ์จะเป็นอีกฟังก์ชันหนึ่งที่อาร์กิวเมนต์แรกถูกลบออก (ประเภทเมื่อเงื่อนไขคือ จริง); มิฉะนั้น ให้ใช้ประเภท never (พิมพ์เมื่อเงื่อนไขเป็นเท็จ)”
  • 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 สุดท้าย เราก็ทำ Conditional Type ให้สมบูรณ์ ถ้า 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