@wordpress/veri Depolarına TypeScript Ekleme

Yayınlanan: 2021-02-05

Geçen yıl TypeScript hakkında çok konuştuk. En son gönderilerimden birinde, WordPress eklentilerinizde TypeScript'i gerçek bir örnek üzerinden nasıl kullanacağınızı ve özellikle seçicilerimize, eylemlerimize ve azaltıcılarımıza türler ekleyerek bir Redux mağazasını nasıl geliştireceğimizi gördük.

Söz konusu örnekte, aşağıdaki gibi temel JavaScript kodundan yola çıktık:

 // Selectors function getPost( state, id ) { … } function getPostsInDay( state, day ) { … } // Actions function receiveNewPost( post ) { … } function updatePost( postId, attributes ) { … } // Reducer function reducer( state, action ) { … }

bize her bir işlevin ne yaptığı ve her parametrenin ne olduğu hakkında ipuçları veren tek şey, aşağıdaki geliştirilmiş TypeScript karşılığına göre adlandırma yeteneklerimize bağlıdır:

 // 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 { … }

her şey düzgün yazıldığından, her şeyi daha net hale getirir:

 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; };

Birkaç hafta önce yeni eklentimiz Nelio Unlocker üzerinde çalışıyordum ve tüm bu teknikleri uygularken bir sorunla karşılaştım. Öyleyse söz konusu sorunu gözden geçirelim ve nasıl üstesinden gelineceğini öğrenelim!

Sorun

Bildiğiniz gibi, mağazamızda tanımladığımız seçicileri ve/veya eylemleri kullanmak istediğimizde, bunları React kancaları ( useSelect ve useDispatch ile) veya daha yüksek dereceli bileşenler ( withSelect ve withDispatch ile) aracılığıyla erişerek yaparız. , tümü @wordpress/data paketi tarafından sağlanır.

Örneğin, az önce gördüğümüz getPost seçicisini ve updatePost eylemini kullanmak istiyorsak, tek yapmamız gereken şöyle bir şey (mağazamızın adının nelio-store olduğunu varsayarsak):

 const Component = ( { postId } ): JSX.Element => { const post = useSelect( ( select ): Post => select( 'nelio-store' ).getPost( postId ); ); const { updatePost } = useDispatch( 'nelio-store' ); return ( ... ); };

Önceki snippet'te, React kancalarını kullanarak seçicilerimize ve eylemlerimize eriştiğimizi görebilirsiniz. Ancak TypeScript, türlerinin ne olduğunu bir yana, bu seçicilerin ve eylemlerin var olduğunu nasıl biliyor?

İşte tam olarak karşılaştığım sorun buydu. Yani, TypeScript'e select('nelio-store') erişiminin sonucunun tüm mağaza seçicilerimizi içeren bir nesne olduğunu ve dispatch('nelio-store') öğesinin mağaza eylemlerimizle bir nesne olduğunu nasıl söyleyebileceğimi bilmek istedim. .

Çözüm

TypeScript hakkındaki son yazımızda polimorfik fonksiyonlardan bahsetmiştik. Polimorfik fonksiyonlar , verilen argümanlara göre farklı dönüş türleri belirlememize izin verir. Pekala, TypeScript polimorfizmi kullanarak belirtebiliriz ki, @wordpress/data paketinin select ya da dispatch yöntemlerini mağazamızın adıyla parametre olarak çağırdığımızda, sırasıyla seçicilerimiz ve eylemlerimiz olur.

Bunu yapmak için, mağazamızı kaydettirdiğimiz dosyaya aşağıdaki gibi bir declare module bloğu eklemeniz yeterlidir:

 // 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; }

ve ardından Selectors ve Actions türlerinin gerçekte ne olduğunu tanımlayın:

 type Selectors = { getPost: ( id: PostId ) => Post | undefined; getPostsInDay: ( day: Day ) => PostId[]; } type Actions = { receiveNewPost: ( post: Post ) => void; updatePost: ( postId: PostId, attributes: Partial<Post> ) => void; }

Buraya kadar çok iyi değil mi? Tek “sorun”, actions zaten bir dizi doğru yazılmış selectors ve eylemimiz olduğunu bildiği için garip görünen Selectors ve Actions türlerini manuel olarak tanımlamamız gerektiğidir…

TypeScript'te işlev türlerini değiştirme

İçe aktardığımız actions ve selectors nesnelerin türlerine bir göz atarsak, TypeScript'in bize şunları söylediğini görürüz:

 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; }

Gördüğünüz gibi türleri, önceki bölümde manuel olarak tanımladığımız türlerin bire bir kopyasıdır. Pekala, neredeyse kesin: seçiciler ilk argümanlarını ( store state , çünkü select 'den bir seçici çağırdığımızda mevcut değildir) eksiktir ve eylemler void ( dispatch yoluyla çağrılan eylemler hiçbir şey döndürmediği için) döndürür.

Bunları, ihtiyacımız olan Selectors ve Actions türlerini otomatik olarak oluşturmak için kullanabilir miyiz?

TypeScript'te bir işlev türünün ilk parametresi nasıl kaldırılır

Bir an için getPost seçicisine odaklanalım. Türü aşağıdaki gibidir:

 // Old type typeof getPost === ( state: State, id: PostId ) => Post | undefined

Az önce söylediğimiz gibi, state parametresine sahip olmayan yeni bir fonksiyon tipine ihtiyacımız var:

 // New type ( id: PostId ) => Post | undefined

Bu nedenle, zaten var olan bir türden yeni bir tür oluşturmak için TypeScript'e ihtiyacımız var. Bu, dilin çeşitli gelişmiş işlevlerini birleştirerek başarılabilir:

 type OmitFirstArg< F > = F extends ( x: any, ...args: infer P ) => infer R ? ( ...args: P ) => R : never;

Karmaşık, ha? Burada neler olduğuna daha yakından bakalım:

  • type OmitFirstArg<F> . Her şeyden önce, yeni bir yardımcı jenerik tip tanımlıyoruz ( OmitFirstArg ) . Genel olarak, genel bir tür, zaten var olan türlerden yeni türler tanımlamamıza izin veren bir türdür. Örneğin, şeylerin listelerini oluşturmanıza izin verdiği için Array<T> türünü muhtemelen biliyorsunuzdur: Array<string> bir dize listesidir, Array<Post> bir Post listesi vb. Bu kavram, OmitFirstArg<F> bir fonksiyonun ilk argümanını kaldıran bir yardımcı tiptir.
  • Bu genel bir tür olduğundan, teorik olarak herhangi bir TypeScript türüyle kullanabiliriz. Yani, OmitFirstArg<string> ve OmitFirstArg<Post> gibi şeyler mümkündür… bu türün yalnızca en az bir argümanı olan işlevlerle kullanılması gerektiğini bildiğimiz halde. Bu yardımcı tipin sadece fonksiyonlarla kullanıldığından emin olmak için onu koşullu tip olarak tanımlayacağız. Koşullu tür, bir koşula dayalı olarak elde edilen türün ne olması gerektiğini belirtelim: “ F , en az bir bağımsız değişkeni (koşulu) olan bir işlevse, ortaya çıkan tür, ilk argümanın kaldırıldığı başka bir işlevdir (koşul olduğunda tür). doğru); aksi takdirde, never türünü kullanın (koşul yanlış olduğunda yazın)."
  • F extends XXX . Bu, koşulu belirtmek için formüldür. F bir dize olduğunu kontrol etmek ister misiniz? Sadece şunu yazın: F extends string . Tereyağından kıl çeker gibi. Peki ya “tek argümanlı bir işlev?” Bu kesinlikle kulağa daha karmaşık geliyor…
  • (x: any, ...args: infer P) => infer R . Bu bir fonksiyon tipidir: argümanlarla (parantez içinde), ardından bir ok ve ardından fonksiyonun dönüş tipi ile başlarız. Bu özel durumda, fonksiyonun bir x argümanına sahip olmasını şart koşuyoruz (spesifik tipi alakasız). Bu tip tanımının iki ilginç biti vardır. Bir yandan, kalan args (varsa) P türlerini yakalamak için rest operatörünü kullanırız. Öte yandan, TypeScript'in tür çıkarımını ( infer ) kullanarak bu P türlerinin gerçekte ne olduğunu ve ayrıca R tam dönüş türünü öğreniriz.
  • ? (...args: P) => R : never ? (...args: P) => R : never . Son olarak, koşullu türü tamamlıyoruz. F bir işlevse, dönüş türü, argümanları P türünden ve dönüş türü R olan yeni bir işlevdir. Değilse, dönüş türü never .

İstediğimiz yeni türü oluşturmak için bu yardımcı türünü şu şekilde kullanabiliriz:

 const getPost = ( state: State, id: PostId ) => Post | undefined; OmitFirstArg< typeof getPost > === ( id: PostId ) => Post | undefined;

ve istediğimizi elde etmeye şimdiden bir adım daha yaklaştık! Burada oyun alanında bu örneği görebilirsiniz.

TypeScript'te bir işlev türünün dönüş türü nasıl değiştirilir?

Cevabı zaten bildiğinizden eminim: bir fonksiyon tipi alan ve yeni bir fonksiyon tipi döndüren yardımcı bir genel tipe ihtiyacımız var. Bunun gibi bir şey:

 type RemoveReturnType< F > = F extends ( ...args: infer P ) => any ? ( ...args: P ) => void : never;

Kolay değil mi? Önceki bölümde yaptığımıza oldukça benzer: args argüman türlerini P (bu sefer en az bir argüman x gerektirmeye gerek yoktur) ve dönüş türünü yok sayarız. F bir işlevse, void döndüren yeni bir işlev döndürün. Aksi takdirde, never iade etmeyin. Mükemmel!

Bunu oyun alanında kontrol et.

TypeScript'te bir nesne türü başka bir nesne türüyle nasıl eşlenir?

Eylemlerimiz ve seçicilerimiz, anahtarları bu eylemlerin ve seçicilerin adları olan ve değerleri işlevlerin kendisi olan iki nesnedir. Bu, bu nesnelerin türlerinin aşağıdaki gibi göründüğü anlamına gelir:

 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; }

Önceki iki bölümde, bir fonksiyon tipini başka bir fonksiyon tipine nasıl dönüştüreceğimizi öğrendik. Bu, yeni türleri şu şekilde elle tanımlayabileceğimiz anlamına gelir:

 type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; };

Ancak, elbette, bu zamanla sürdürülebilir değil: Her iki türde de işlevlerin adlarını manuel olarak belirliyoruz. Açıkçası, actions ve selectors orijinal tür tanımlarını yeni türlere otomatik olarak eşlemek istiyoruz.

TypeScript'te bunu şu şekilde yapabilirsiniz:

 type OmitFirstArgs< O > = { [ K in keyof O ]: OmitFirstArg< O[ K ] >; } type RemoveReturnTypes< O > = { [ K in keyof O ]: RemoveReturnType< O[ K ] >; }

Umarım, bu zaten mantıklıdır, ancak yine de önceki snippet'in ne yaptığını hızlıca çözelim:

  • type OmitFirstArgs<O> . O nesnesini alan yeni bir yardımcı genel tür oluşturuyoruz.
  • Sonuç, başka bir nesne türüdür (kıvrımlı parantezler {...} yi gösterdiği gibi).
  • [K in keyof O] . Yeni nesnenin sahip olacağı tam anahtarları bilmiyoruz, ancak bunların O bulunanlarla aynı anahtarlar olması gerektiğini biliyoruz. Yani TypeScript'e söylediğimiz şey bu: keyof O bir anahtarı olan tüm K anahtarlarını istiyoruz.
  • Ve sonra, her K anahtarı için türü OmitFirstArg<O[K]> . Yani orijinal tipi ( O[K] ) alıyoruz ve tanımladığımız yardımcı tip (bu durumda OmitFirstArg ) kullanarak istediğimiz tipe dönüştürüyoruz.
  • Son olarak, RemoveReturnTypes ve orijinal yardımcı tür RemoveReturnType ile aynısını yapıyoruz.

Seçicilerimiz ve eylemlerimizle @wordpress/data data'yı genişletme

Bugün gördüğümüz dört yardımcı türü bir global.d.ts dosyasına ekler ve projenizin köküne kaydederseniz, orijinal sorunu çözmek için nihayet bu gönderide gördüğümüz her şeyi birleştirebilirsiniz:

 // 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; }

Ve bu kadar! Umarım bu geliştirici ipucunu beğenmişsinizdir ve beğendiyseniz lütfen meslektaşlarınız ve arkadaşlarınızla paylaşın. Ah! Aynı sonucu elde etmek için farklı bir yaklaşım biliyorsanız, yorumlarda bana bildirin.

Unsplash'ta Gabriel Crismariu'nun Öne Çıkan Resmi.