emitter
emitter copied to clipboard
Add support to infer receiver payload types
I'm submitting a...
[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[ ] Performance issue
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => https://github.com/ngxs/store/blob/master/CONTRIBUTING.md
[ ] Other... Please describe:
Current behavior
I would love some functionality that enables the typescript compiler to ensure that the payload parameters matches the receiver function and the emitters both when using @Emitter(...)
decorator and when I am using the EmitterService.action(...)
method. Now we have to manually ensure that the payload type of the receiver and that we use for the emitters matches or we get a runtime error.
Expected behavior
We could introduce some utility types that extract the payload type based on the receiver method like into the project. I have created the following types that I am using in a project I'm working on that might work:
type EmitterFunctionBase = (ctx: any, action: EmitterAction<any>) => any;
type EmittableParams<T extends EmitterFunctionBase> =
T extends (ctx: any, action: infer TAction) => any ? (
TAction extends EmitterAction<infer TPayload> ? TPayload : void
) : never;
declare type EmittableFunction<T extends EmitterFunctionBase, U = any> = Emittable<EmittableParams<T>, U>;
If we introduced those types. we could also add the following overload to the EmitterService.action
method to get type inference when using the EmitterService
directly:
export class EmitterService {
// ...
action<T extends EmitterFunctionBase, U = any>(receiver: T): Emittable<EmittableParams<T>, U>;
action<T = void, U = any>(receiver: Function): Emittable<T, U> {
// ...
}
// ...
}
We would be able to use receivers without explicitly typing the payload type where we are using them. Here is an example of a consumer of the receive would look with these proposed types:
export class MyState {
@Receiver()
static doStuff(ctx: StateContext<MyState>, action: EmitterAction<number>) {
ctx.patchState({
count: ctx.getState().count + action.payload
});
}
}
export class MyComponent {
@Emitter(MyState.doStuff)
increment: EmittableFunction<typeof MyState.doStuff>;
constructor(emitter: EmitterService) {
emitter.action(MyState.doStuff).emit(5); // The payload type is automatically inferred because we match the `EmitterFunctionBase` overload
// emitter.action(MyState.doStuff).emit({ foo: true }); // This causes an compiler error
}
click($event) {
this.increment.emit(5);
// this.increment.emit({foo: true}); // This also causes an compiler error
}
}
What is the motivation / use case for changing the behavior?
This would make refactoring easier since the compiler can catch type errors when we change the payload type of a receiver without us having to manually search and update all the types every place the receiver is referenced
Others:
The extra overload of the EmitterService.action
method might cause breaking changes if someone uses a payload type matching the EmitterFunctionBase
type because then typescript would match and extract the parameters instead of using the type as is, but otherwise I think this would just be an improvement of the library ergonomics if the users opt into it.
If you would like to, I can create a pull request where I introduce the types and overload into the project
I created a repository at https://github.com/Andreas-Hjortland/ngxs-emitter-types where I added the type enhancements so that you can see how it works.. If you change the signature of any of the receivers (for instance, change the removePost
payload to be the id instead of a whole post, you will see that we get compiler errors without having to find all references to the removePost
and updating the types there)