Правильная защита типов пользовательских конвейерных операторов RxJS
Я работаю с API, который имеет фиксированную, непротиворечивую структуру ответов: это всегда объект, который имеет data
собственность на это. Поскольку это очень утомительно и слишком явно, чтобы постоянно отображать данные в запросах RxJS (или эффектах ngrx), я решил ввести пользовательский оператор RxJS, который собирает данные и применяет необязательный обратный вызов.
Но теперь некоторые из моих эффектов жалуются на информацию о типе (например: property x doesn't exist on type {}
), поэтому я полагаю, что моих усилий по правильной защите ввода-вывода оператора недостаточно:
export function mapData<T, R>(callback?: (T) => R) {
return (source: Observable<T>) => source.pipe(
map(value => value['data'] as R), // isn't that an equivalent of `pluck<T>('data')` ?
map(value => typeof callback === 'function' ? callback(value) : value as R),
);
}
Пример эффекта ngrx с проблемами защиты типов:
switchMap(() => this.api.getData().pipe(
mapData(),
mergeMap(data => [
new actions.DataSuccessAction({ id: data.id }), // <-- id does not exist on type {}
new actions.SomeOtherAction(data),
]),
catchError(err => of(new actions.DataFailureAction(err))),
)),
Что, конечно, исчезнет, когда я приведу это явно:
mapData<any, IMyData>(....),
Я хотел бы услышать, является ли это правильным, TypeScript способ делать вещи.
1 ответ
Вы можете использовать несколько перегрузок для моделирования различных типов поведения. Я не уверен на 100%, каково должно быть поведение, это не на 100% ясно из вашего вопроса, но мое прочтение подсказывает следующие правила:
- Если
T
имеетdata
и нетcallback
указан возвратdata
- Если мы укажем
callback
тогда возвращение продиктованоcallback
- Если нет
callback
указано и 1 не применяется, просто вернутьT
Перегруженная версия будет выглядеть примерно так:
export function mapData<T, R>(callback: (data: T) => R) : OperatorFunction<T, R>
export function mapData<T extends { data: any }>() : OperatorFunction<T, T['data']>
export function mapData<T>() : OperatorFunction<T, T>
export function mapData<T extends { data? : undefined } | { data: R }, R>(callback?: (data: T) => R) {
return (source: Observable<T>) => source.pipe(
map(value => typeof callback === 'function' ? callback(value) : (value.data ? value.data : value)),
);
}
// Tests
of({ data: { id: 0 }}).pipe(
mapData(),
mergeMap(data => [
new actions.DataSuccessAction({ id: data.id }), // <-- id does not exist on type {}
new actions.SomeOtherAction(data),
]),
catchError(err => of(new actions.DataFailureAction(err))),
)
of({ other: { id: 0 }}).pipe(
mapData(d =>d.other),
mergeMap(data => [
new actions.DataSuccessAction({ id: data.id }), // <-- id does not exist on type {}
new actions.SomeOtherAction(data),
]),
catchError(err => of(new actions.DataFailureAction(err))),
)
of({ data: { id: 0 }}).pipe(
mapData(d =>d.data),
mergeMap(data => [
new actions.DataSuccessAction({ id: data.id }), // <-- id does not exist on type {}
new actions.SomeOtherAction(data),
]),
catchError(err => of(new actions.DataFailureAction(err))),
)
// Filler classes
namespace actions {
export class DataSuccessAction<T>{
constructor(public data:T){}
}
export class SomeOtherAction<T>{
constructor(public data:T){}
}
export class DataFailureAction<T>{
constructor(public data:T){}
}
}