reactjs - `createAction` tarafından döndürülen tür `ofType` ile korunmuyor
Kullanıcının mevcut atıştırmalık listesinden atıştırmalıkları seçmesine izin veren bir uygulama yapıyorum. Atıştırmalıklar harici bir API'den yükleniyor.
redux-observable kullanarak aksiyonları “dinliyorum” ve ardından uygun API isteklerini dispatch ediyorum. İşte mevcut kodum, problemli satır hakkında bir yorum ile:
// actions.ts
import { createAction } from '@reduxjs/toolkit';
import * as Type from './types';
const pfx = '[Snacks]';
export const snackSelect = createAction<Type.Snack['id']>(`${pfx} Select`);
export const snackSelectSuccess = createAction(`${pfx} Select success`);
export const snacksAvailableLoad = createAction(`${pfx} Available load`);
export const snacksAvailableLoadSuccess = createAction<Array<Type.Snack>>(`${pfx} Available load success`);
// epics.ts
import {
combineEpics,
ofType,
} from 'redux-observable';
import {
map,
switchMap,
} from 'rxjs/operators';
import * as API from '../api';
import * as Actions from './actions';
export default combineEpics(
action$ => action$.pipe(
ofType(Actions.snacksAvailableLoad),
switchMap(API.snacksGet),
map(Actions.snacksAvailableLoadSuccess),
),
action$ => action$.pipe(
ofType(Actions.snackSelect),
switchMap(({ payload }) => API.snackSelect(payload)), // Error: Property 'payload' does not exist on type 'Action<any>'
map(Actions.snackSelectSuccess),
)
);
payload (hedef atıştırmalık idsi olması gereken) benim epikimin switchMap'inde mevcut değil.
switchMap((action: ReturnType<typeof Actions.snackSelect>) => API.snackSelect(action.payload)) ile zorlayabilirim, ama bu ReturnType<typeof> sanki kod kokusu veya oldukça fazla bir kalıp kod gibi görünüyor.
ofType aracılığıyla tipi korumanın doğru bir yolu var mı?
Cevap #1
Redux-observable ile çok aşina değilim, ama ofType fonksiyonundaki filtrelemenin Action['type'] özelliğinin türüne dayandığı görünüyor. Tür fonksiyon bildirimiyle ilgili yorumlarıma bakın:
export declare function ofType<
T extends Action, // input action type
R extends T = T, // output action type
K extends R['type'] = R['type'] // type name to filter by
>(...key: K[]): (source: Observable<T>) => Observable<R>;
İlk sorun, TypeScript sürümünüze bağlı olarak, eylemlerinizin type özelliğinin yalnızca string olması ve "[Snacks] Select success" gibi bir literal değer olmamasıdır. $ {pfx} Select success gibi bir şablonla string oluştururken, string'e genişlemeyi önlemek için as const eklemeniz gerekir. Bu, şu anda beta aşamasında olan v4.3.0+ sürümünde gerekli değildir.
as const eklemek, yükü olmayan iki eyleminizi düzeltecektir. Yükü olan iki eylem ise hala string olacaktır. Bunun nedeni, createAction işlevinin yük P ve tür T için iki genel tür parametresine sahip olmasıdır. Bir parametreyi belirleyip diğerinin çıkarılmasına izin veremezsiniz. Bu nedenle yük için bir genel tür belirlediğinizde, dizgi sabiti türünü çıkarma yeteneğini kaybedersiniz ve varsayılan değer olan string'e geri döner.
Her zaman ikisini de manuel olarak belirtebilirsiniz:
export const snackSelect = createAction<Type.Snack["id"], `${typeof pfx} Select`>(
`${pfx} Select` as const
);
Ya da burada tutarlı bir deseniniz olduğu için bir tür yardımcı işlev oluşturabilirsiniz.
İkinci konu, ofType’ın sadece tür adı K’dan eylem türü R’yi çıkarabilme yeteneğidir. Eğer başlangıç eylem türünüz T özel eylem türlerinizin bir birleşimi ise, bu muhtemelen {type: K} ile atanabilir olan birleşen üyeleri çıkararak otomatik olarak gerçekleşir. Ancak T genel bir any eylem ise bunu ayırt edemez.
Yani bir tür koruyucu kullanmanız gerekecek. Redux-toolkit eylemleri, yerleşik bir tür koruyucu fonksiyon olan match ile birlikte gelir.
declare interface BaseActionCreator<P, T extends string, M = never, E = never> {
type: T;
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>;
}
...ve görünüşe göre ihtiyacın olan tek şey bu ve bu aynı zamanda ilk sorunu da çözüyor.
Cevap #2
Eğer filter ve .match kullanırsam, o zaman doğru tür çıkarımlanır:
export default combineEpics(
action$ => action$.pipe(
filter(Actions.snacksAvailableLoad.match),
switchMap(API.snacksGet),
map(Actions.snacksAvailableLoadSuccess),
),
action$ => action$.pipe(
filter(Actions.snackSelect.match),
switchMap(({ payload }) => API.snackSelect(payload)),
map(Actions.snackSelectSuccess),
),
);
