ngxs-labs / emitter

:octopus: New pattern that provides the opportunity to feel free from actions
MIT License
110 stars 5 forks source link

Add support to infer receiver payload types #786

Open Andreas-Hjortland opened 1 year ago

Andreas-Hjortland commented 1 year ago

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

Andreas-Hjortland commented 1 year ago

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)