Menambahkan TypeScript ke @wordpress/data Stores
Diterbitkan: 2021-02-05Tahun lalu kami berbicara banyak tentang TypeScript. Dalam salah satu posting terbaru saya, kami melihat cara menggunakan TypeScript di plugin WordPress Anda melalui contoh nyata dan, khususnya, cara meningkatkan toko Redux dengan menambahkan jenis ke pemilih, tindakan, dan reduksi kami.
Dalam contoh tersebut, kami beralih dari kode JavaScript dasar seperti ini:
// Selectors function getPost( state, id ) { … } function getPostsInDay( state, day ) { … } // Actions function receiveNewPost( post ) { … } function updatePost( postId, attributes ) { … } // Reducer function reducer( state, action ) { … }di mana satu-satunya hal yang memberi kami petunjuk tentang apa yang dilakukan setiap fungsi dan apa setiap parameter bergantung pada kemampuan penamaan kami, ke rekan TypeScript yang ditingkatkan berikut:
// 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 { … }yang membuat semuanya lebih jelas, karena semuanya diketik dengan benar:
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; };Beberapa minggu yang lalu saya sedang mengerjakan plugin baru kami, Nelio Unlocker, dan saya mengalami masalah saat menerapkan semua teknik ini. Jadi mari kita tinjau masalah tersebut dan pelajari cara mengatasinya!
Masalah
Seperti yang mungkin sudah Anda ketahui, ketika kami ingin menggunakan selektor dan/atau tindakan yang kami definisikan di toko kami, kami melakukannya dengan mengaksesnya melalui kait React (dengan useSelect dan useDispatch ) atau melalui komponen tingkat tinggi (dengan withSelect dan withDispatch ) , yang semuanya disediakan oleh paket @wordpress/data .
Misalnya, jika kita ingin menggunakan pemilih getPost dan tindakan updatePost yang baru saja kita lihat, yang harus kita lakukan adalah seperti ini (dengan asumsi toko kita bernama nelio-store ):
const Component = ( { postId } ): JSX.Element => { const post = useSelect( ( select ): Post => select( 'nelio-store' ).getPost( postId ); ); const { updatePost } = useDispatch( 'nelio-store' ); return ( ... ); };Dalam cuplikan sebelumnya Anda dapat melihat kami mengakses pemilih dan tindakan kami menggunakan kait Bereaksi. Tapi bagaimana TypeScript tahu bahwa pemilih dan tindakan itu ada, apalagi tipenya?
Nah, itulah masalah yang saya hadapi. Yaitu, saya ingin tahu bagaimana saya bisa memberi tahu TypeScript bahwa hasil mengakses select('nelio-store') adalah objek yang berisi semua pemilih toko kami dan dispatch('nelio-store') adalah objek dengan tindakan toko kami .
Solusinya
Dalam posting terakhir kami di TypeScript, kami berbicara tentang fungsi polimorfik. Fungsi polimorfik memungkinkan kita untuk menentukan tipe pengembalian yang berbeda berdasarkan argumen yang diberikan. Nah, dengan menggunakan polimorfisme TypeScript kita dapat menentukan bahwa, ketika kita memanggil metode select atau dispatch paket @wordpress/data dengan nama toko kita sebagai parameter, hasil yang kita dapatkan adalah penyeleksi dan tindakan kita masing-masing.
Untuk melakukan ini, cukup tambahkan blok declare module di file tempat kami mendaftarkan toko kami sebagai berikut:
// 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; } dan kemudian tentukan apa sebenarnya tipe Selectors dan 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; } Sejauh ini, sangat baik, bukan? Satu-satunya "masalah" adalah bahwa kita harus secara manual menentukan jenis Selectors dan Actions , yang terdengar aneh mengingat TypeScript sudah tahu bahwa kita memiliki satu set selectors dan actions yang diketik dengan benar ...
Memanipulasi tipe fungsi dalam TypeScript
Jika kita melihat pada jenis actions dan objek selectors yang kita impor, kita akan melihat bahwa TypeScript memberi tahu kita hal berikut:
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; } Seperti yang Anda lihat, tipenya adalah salinan persis dari tipe yang kita definisikan secara manual di bagian sebelumnya. Yah, hampir tepat: penyeleksi kehilangan argumen pertama mereka ( state toko, karena tidak ada saat kita memanggil pemilih dari select ) dan tindakan mengembalikan void (karena tindakan yang dipanggil melalui dispatch tidak menghasilkan apa-apa).
Bisakah kita menggunakannya untuk secara otomatis menghasilkan tipe Selectors dan Actions yang kita butuhkan?
Cara menghapus parameter pertama dari tipe fungsi di TypeScript
Mari kita fokus sejenak pada pemilih getPost . Jenisnya adalah sebagai berikut:
// Old type typeof getPost === ( state: State, id: PostId ) => Post | undefined Seperti yang baru saja kami katakan, kami membutuhkan tipe fungsi baru yang tidak memiliki parameter state :
// New type ( id: PostId ) => Post | undefinedJadi, kita membutuhkan TypeScript untuk menghasilkan tipe baru dari tipe yang sudah ada. Ini dapat dicapai dengan menggabungkan beberapa fungsi lanjutan bahasa:
type OmitFirstArg< F > = F extends ( x: any, ...args: infer P ) => infer R ? ( ...args: P ) => R : never;Rumit, ya? Mari kita lihat lebih dekat apa yang terjadi di sini:
-
type OmitFirstArg<F>. Pertama-tama, kita mendefinisikan tipe generik bantu baru (OmitFirstArg) . Secara umum, tipe generik adalah tipe yang memungkinkan kita mendefinisikan tipe baru dari tipe yang sudah ada. Misalnya, Anda mungkin akrab dengan tipeArray<T>, karena memungkinkan Anda membuat daftar hal-hal:Array<string>adalah daftar string,Array<Post>adalah daftarPost, dll. Nah, berikut gagasan ini,OmitFirstArg<F>adalah tipe pembantu yang menghilangkan argumen pertama dari suatu fungsi. - Karena ini adalah tipe generik, kami secara teoritis dapat menggunakannya dengan tipe TypeScript lainnya. Artinya, hal-hal seperti
OmitFirstArg<string>danOmitFirstArg<Post>dimungkinkan… meskipun kita tahu tipe ini hanya boleh digunakan dengan fungsi yang memiliki setidaknya satu argumen. Untuk memastikan tipe pembantu ini digunakan dengan fungsi saja, kami akan mendefinisikannya sebagai tipe bersyarat. Tipe kondisional mari kita tentukan tipe yang dihasilkan harus didasarkan pada suatu kondisi: “jikaFadalah fungsi dengan setidaknya satu argumen (kondisi), tipe yang dihasilkan adalah fungsi lain di mana argumen pertama telah dihapus (ketik ketika kondisi adalah benar); jika tidak, gunakan tipenever(ketik ketika kondisinya salah).” -
F extends XXX. Ini adalah rumus untuk menentukan kondisi. Apakah Anda ingin memeriksa bahwaFadalah string? Cukup ketik:F extends string. Mudah. Tapi bagaimana dengan "fungsi dengan satu argumen?" Itu pasti terdengar lebih rumit… -
(x: any, ...args: infer P) => infer R. Ini adalah tipe fungsi: kita mulai dengan argumen (dalam tanda kurung), diikuti dengan panah, diikuti dengan tipe pengembalian fungsi. Dalam kasus khusus ini, kami mengharuskan fungsi tersebut memiliki satu argumenx(yang tipe spesifiknya tidak relevan). Definisi tipe ini memiliki dua bit yang menarik. Di satu sisi, kami menggunakan operator sisanya untuk menangkap tipePdariargsyang tersisa (jika ada). Di sisi lain, kami menggunakan inferensi tipe TypeScript (infer) untuk mengetahui apa tipePitu sebenarnya, serta tipe pengembalian yang tepatR. -
? (...args: P) => R : never? (...args: P) => R : never. Akhirnya, kami menyelesaikan tipe kondisional. JikaFadalah sebuah fungsi, tipe kembalian adalah fungsi baru yang argumennya bertipePdan tipe kembaliannya adalahR. Jika tidak, tipe pengembaliannyanever.
Ini adalah bagaimana kita dapat menggunakan tipe pembantu ini untuk membuat tipe baru yang kita inginkan:

const getPost = ( state: State, id: PostId ) => Post | undefined; OmitFirstArg< typeof getPost > === ( id: PostId ) => Post | undefined;dan kita sudah selangkah lebih dekat untuk mencapai apa yang kita inginkan! Di sini Anda dapat melihat contoh ini di taman bermain.
Bagaimana mengubah tipe kembalinya tipe fungsi di TypeScript
Saya yakin Anda sudah tahu jawabannya dengan mengetahui: kita membutuhkan tipe generik tambahan yang mengambil tipe fungsi dan mengembalikan tipe fungsi baru. Sesuatu seperti ini:
type RemoveReturnType< F > = F extends ( ...args: infer P ) => any ? ( ...args: P ) => void : never; Mudah, bukan? Ini sangat mirip dengan apa yang kami lakukan di bagian sebelumnya: kami menangkap tipe args di P (kali ini tidak perlu memerlukan setidaknya satu argumen x ) dan mengabaikan tipe pengembalian. Jika F adalah suatu fungsi, kembalikan fungsi baru yang mengembalikan void . Jika tidak, kembali never . Luar biasa!
Lihat ini di taman bermain.
Cara memetakan tipe objek ke tipe objek lain di TypeScript
Tindakan kami dan pemilih kami adalah dua objek yang kuncinya adalah nama tindakan dan penyeleksi tersebut dan yang nilainya adalah fungsi itu sendiri. Ini berarti bahwa jenis objek ini terlihat seperti berikut:
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; }Dalam dua bagian sebelumnya, kita telah mempelajari cara mengubah satu jenis fungsi menjadi jenis fungsi lainnya. Ini berarti bahwa kita dapat mendefinisikan tipe baru dengan tangan seperti ini:
type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; }; Tapi, tentu saja, ini tidak berkelanjutan dari waktu ke waktu: kami secara manual menentukan nama fungsi di kedua jenis. Jelas, kami ingin secara otomatis memetakan definisi tipe asli dari actions dan selectors ke tipe baru.
Inilah cara Anda melakukannya di TypeScript:
type OmitFirstArgs< O > = { [ K in keyof O ]: OmitFirstArg< O[ K ] >; } type RemoveReturnTypes< O > = { [ K in keyof O ]: RemoveReturnType< O[ K ] >; }Mudah-mudahan, ini sudah masuk akal, tetapi mari kita cepat mengungkap apa yang dilakukan cuplikan sebelumnya:
-
type OmitFirstArgs<O>. Kami membuat tipe generik bantu baru yang mengambil objekO. - Hasilnya adalah jenis objek lain (seperti yang ditunjukkan oleh kurung kurawal
{...}). -
[K in keyof O]. Kami tidak tahu persis kunci yang akan dimiliki objek baru, tetapi kami tahu mereka harus menjadi kunci yang sama dengan yang disertakan dalamO. Jadi itulah yang kami beri tahu TypeScript: kami ingin semua kunciKyang merupakan kunci darikeyof O. - Dan kemudian, untuk setiap kunci
K, tipenya adalahOmitFirstArg<O[K]>. Artinya, kita mendapatkan tipe asli (O[K]) dan kita mengubahnya menjadi tipe yang kita inginkan menggunakan tipe bantu yang kita definisikan (dalam hal ini,OmitFirstArg). - Terakhir, kami melakukan hal yang sama dengan
RemoveReturnTypesdan tipe bantu asliRemoveReturnType.
Memperluas @wordpress/data dengan pemilih dan tindakan kami
Jika Anda menambahkan empat jenis tambahan yang telah kita lihat hari ini di file global.d.ts dan menyimpannya di root proyek Anda, Anda akhirnya dapat menggabungkan semua yang telah kita lihat di posting ini untuk menyelesaikan masalah aslinya:
// 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; }Dan itu saja! Saya harap Anda menyukai tip dev ini dan, jika Anda menyukainya, silakan bagikan dengan kolega dan teman Anda. Oh! Dan jika Anda tahu pendekatan yang berbeda untuk mendapatkan hasil yang sama, beri tahu saya di komentar.
Gambar Unggulan oleh Gabriel Crismariu di Unsplash.
