正确类型保护自定义 RxJS 管道运算符
Properly type-guarding custom RxJS pipeable operators
我正在使用具有固定、一致的响应结构的 API:它始终是一个带有 data
属性 的对象。由于不断映射 RxJS 请求(或 ngrx 效果)中的数据非常烦人且过于明确,因此我决定引入一个自定义 RxJS 运算符来提取数据并应用可选的回调。
但是现在我的一些效果会抱怨类型信息(比如:property x doesn't exist on type {}
),所以我想我努力正确地保护运算符的 I/O 是不够的:
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 处理方式。
您可以使用多个重载来模拟不同类型的行为。我不是 100% 确定行为应该是什么,你的问题也不是 100% 清楚,但我对它的阅读表明了以下规则:
- 如果
T
有 data
而没有指定 callback
return data
- 如果我们指定
callback
那么 return 由 callback
决定
- 如果未指定
callback
且 1 不适用,则 return 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){}
}
}
我正在使用具有固定、一致的响应结构的 API:它始终是一个带有 data
属性 的对象。由于不断映射 RxJS 请求(或 ngrx 效果)中的数据非常烦人且过于明确,因此我决定引入一个自定义 RxJS 运算符来提取数据并应用可选的回调。
但是现在我的一些效果会抱怨类型信息(比如:property x doesn't exist on type {}
),所以我想我努力正确地保护运算符的 I/O 是不够的:
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 处理方式。
您可以使用多个重载来模拟不同类型的行为。我不是 100% 确定行为应该是什么,你的问题也不是 100% 清楚,但我对它的阅读表明了以下规则:
- 如果
T
有data
而没有指定callback
returndata
- 如果我们指定
callback
那么 return 由callback
决定
- 如果未指定
callback
且 1 不适用,则 returnT
重载版本看起来像这样:
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){}
}
}