I have a custom operator, waitFor
which I am using in my effects as so:
public effect$: Observable<Action> = createEffect(() => {
return this.actions$.pipe(
ofType(myAction),
waitFor<ReturnType<typeof myAction>>([anotherAction]),
...etc
);
});
It basically looks at correlationId's to not continue executing until the array of actions have been dispatched. But that is beside the point here.
As expected ofType
takes the source observable and uses this as the return type, however I am struggling to achieve the same effect. As you can see above I am using ReturnType<typeof myAction>>
and the following in my waitFor
method:
export function waitFor<A extends Action>(actionsToWaitFor$: Array<Actions>): OperatorFunction<A, A> {
So at the moment if I call waitFor
like this:
public effect$: Observable<Action> = createEffect(() => {
return this.actions$.pipe(
ofType(myAction),
waitFor([anotherAction]),
...etc
);
});
Then its type is inferred as Action
, but I want this to be ReturnType<typeof theSourceObservable>
by default. So I would assume I would need something like this in my waitFor
method:
export function waitFor<A extends ReturnType<typeof sourceObservable?!>>(actionsToWaitFor$: Array<Actions>): OperatorFunction<A, A> {
waitFor
looks like this:
export function waitFor<A extends Action>(actionsToWaitFor$: Array<Actions>): OperatorFunction<A, A> {
return (source$) => {
return source$.pipe(
switchMap((action: A & { correlationId: string}) => {
// use zip() to wait for all actions
// and when omitting map((action) => action)
// so the original action is always returned
})
);
};
}
From the ofType
source, it looks like I need to use Extract
StackBlitz example is shown here
This, at least, compiles; I don't know if it also does what you need.
public effect3$: Observable<Action> = createEffect(() => {
const a:Action[]= []
return this.actions$.pipe(
ofType(doSomething),
this.someCustomOperatorReturningStaticTypes(),
this.thisWontWork(a),
tap(({aCustomProperty}) => {
// The type is inferred
console.log(aCustomProperty);
}),
)
});
private thisWontWork<A extends Action>(actionsToWaitFor$: Action[]): OperatorFunction<A, A> {
return (source$) => {
return source$.pipe(
tap(() => {
console.log('Should work')
})
)
}
}
I wasn't able to run it in the StackBlitz, any hint?
Hope this helps
Came across this question and I thought I'd add a couple explanations in case you are still wondering and if anyone comes across this in future!
If you want to infer something as the type it is passed as (e.g here you want your action to be returned as the type it is) you just return the same generic which you pass in - as the answer says like:
function foo<A>(val: A): A {
return val;
}
If I call foo() with a number, I get a number back. If I call it with a specific action type, then the same thing happens. This is why you just need the return type OperatorFunction<A, A>
- same thing in, same thing out.
The key here is that you need some set of actions which you are narrowing.
Using createAction takes care of this for you, but for example's sake I'll create this by hand.
type FooAction = { type: 'FOO_ACTION' };
type BarAction = { type: 'BAR_ACTION' };
// This type gets built by you using createAction - this is your Action type
type MyActions = FooAction | BarAction;
Next we need a type which is going to take our set of actions (MyActions
) - and narrow it to a specific action based on the type.
type NarrowActionType<T extends { type: any }, K extends string> = T extends { type: K }
? T
: never;
We can verify this works:
type NarrowedActionTest = NarrowActionType<MyActions, 'FOO_ACTION'>;
ofType is a higher order function which first accepts a string, and then the observable stream to operate on. I'm not going to use observables to keep it simpler, but it's the same principle.
In essence we want something along the lines of (type: string) => (action: Action) => action is NarrowActionType
for the type signature.
A
T
T
extends stringT
to begin with, and then the type of A
once the object stream is applied, so let's use one generic parameter at each function applicationA.type
is the same as type. So we should enforce this exists, with A extends { type: any }
function isOfType<T extends string>(type: T) {
return <A extends { type: any }>(action: A): action is NarrowActionType<A, T> => {
return action.type === type
}
}
Now we can test this function on a MyActions type, and see if it can narrow the output:
const myAction: MyActions = { type: 'FOO_ACTION' };
const isFooAction = isOfType('FOO_ACTION')(myAction);
if (isOfType('FOO_ACTION')(myAction)) {
type myNarrowedAction = typeof myAction; // is FooAction
}
Full playground link here: https://www.typescriptlang.org/play/#code/C4TwDgpgBAcghgJwQewO4EEDGwCWyB2AKuBADyFQQAewE+AJgM5QDeUokAXFHPiFAF8ANFADSlGnSZRGwBDnwBzAHxQAvFArVaDZmw4Ru4gQCgoUAPyazUbvggA3CAgDcJkwagAxZMiy4CdVZ2Em4Aci8AeUiAfXQAYUIASUiYMME3TwAhRH88fCD9UKgwrPQAJTjElLSM9xMAegbNAAscZk9FCGBmACMAVxwAG2AoXv4QZH6ofsYFRShMBAg4WjzAgFp2NuZ2qEn+hCh1goMPEigAWRAT5g0fP2x8qAAfKByEE7dzyFhEFFQEHoJ0IEFkQXgSDQIJIpGutxEEWiVWSqTCym+ADN+vgnoF2pFMcRIOQJDppLJ5EplAAKAzcQgASlYNnMy2AhwKpHQZKkehCXB4fEEtLgePw3HQjO4YoCBT2kIBMJJ6BEhFUalULFZ5l17M5PHFADpPGozQKIDrTKYTJgCOCALY3cXceHiu7BeklKKxBKo2oCNx2-Dg9oPE5BAlEkg0pG+6poxk0p0nRnfHCYqA0qPEiCxn0omphJMp8WM5na3UWqBOxVoIERjQGZCZ0tylxQJpQPbh8UmUxAA
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With