Open tjfryan opened 5 years ago
The pipeable version (works only with TS 3.4):
declare module 'redux-observable' {
export function ofType<T extends Action, A extends Array<Nullable<T['type']>>>(
...key: A
): (source: Observable<T>) => Observable<ActionByType<T, A[number]>>;
}
I think 3.3 was released in January. My only issue is whether or not people have already upgraded or are willing to do so at large. I don't think there's a way to support older versions at the same time?
You can probably add a postinstall
script that would patch the type definitions depending on the found version of TS. 🤣
The pipeable version stopped working with TS 3.5.1. Fixed it by replacing Action
with Action<string>
:
declare module "redux-observable" {
type ActionByType<Union, Type> = Union extends { type: Type } ? Union : never;
export function ofType<
TAction extends Action<string>,
TActionTypes extends Array<TAction["type"]>
>(
...key: TActionTypes
): (
source: Observable<TAction>
) => Observable<ActionByType<TAction, TActionTypes[number]>>;
}
Another way to define it is:
declare module 'redux-observable' {
type ActionByType<Union, Type> = Union extends { type: Type } ? Union : never;
export function ofType<
TActionTypes extends string[]
>(
...key: TActionTypes
): <TAction extends Action<string>>(
source: Observable<TAction>
) => Observable<ActionByType<TAction, TActionTypes[number]>>;
}
Defined this way, it works correctly even when invoked without pipe
(e.g. ofType("bar")(actions$)
).
UPD: the first way is better after all, the autocomplete is more helpful:
Would love this to see the light 😍
I was wondering for this a while, nice job @thorn0!!!
I found an optimized way to declare ofType
that could narrow the type of output based on the param.
function myOfType<
Input extends AnyAction,
// Note: Without letting `Type` extending string, Type cannot be inferred to a literal type.
Type extends Input['type'] & string,
Output extends Input = Extract<Input, Action<Type>>,
>(...types: Type[]): OperatorFunction<Input, Output> {
return filter((input): input is Output => {
eturn types.indexOf(input.type) >= 0;
});
}
And then the filtered action type can be narrowed.
type InputAction = {type: 'a'; foo: number} | {type: 'b'; bar: string} | {type: 'c'; foo: number};
const observable = of<InputAction[]>({type: 'a', foo: 1}, {type: 'b', bar: 'bar'}, {type: 'a', foo: 2});
observable.pipe(
myOfType('a'),
tap((value) => console.log(value.foo)), // okay
tap((value) => console.log(value.bar)), // compile failure
);
// compared to the builtin `ofType`, unless type param of `ofType` is explicit specified,
// the type of output is same with input
observable.pipe(
ofType('a'),
tap((value) => console.log(value.foo)), // compile failure, foo is not a member of InputAction
);
Unfortunately, I found that the action type auto-complete seems to be impossible with my own ofType
when the output type is narrowed, after trying many tricky ways. However, I think it's still worth do it this way, as the output action type is inferred.
What is the current behavior?
When using redux-observable in TypeScript, providing a key or keys to
ActionsObservable.prototype.ofType
doesn't refine the type of the actions emitted and often requires a redundantaction.type
check in order to maintain type correctness.What is the expected behavior?
With TypeScript 3.3, it is possible for the type system to automatically refine the Action union to only the options that match the provided keys. Here's a rough demo
I'm currently using the following custom typedef as a stop-gap in my codebase.