การเพิ่ม 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
