Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ofType type declaration for TS >3.3 #622

Open
tjfryan opened this issue Mar 1, 2019 · 7 comments
Open

Improve ofType type declaration for TS >3.3 #622

tjfryan opened this issue Mar 1, 2019 · 7 comments

Comments

@tjfryan
Copy link

tjfryan commented Mar 1, 2019

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 redundant action.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.

type ActionByType<Union, Type> = Union extends {type: Type} ? Union : never;

declare module 'redux-observable' {
  interface ActionsObservable<T extends Action> {
    ofType<A extends Array<Nullable<T['type']>>>(
      ...keys: A
    ): ActionsObservable<ActionByType<T, A[number]>>;
  }
}
@thorn0
Copy link

thorn0 commented Apr 22, 2019

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]>>;
}

@jayphelps
Copy link
Member

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?

@thorn0
Copy link

thorn0 commented Apr 23, 2019

You can probably add a postinstall script that would patch the type definitions depending on the found version of TS. 🤣

@thorn0
Copy link

thorn0 commented Jun 2, 2019

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:
image

@donifer
Copy link

donifer commented Jun 26, 2019

Would love this to see the light 😍

@leoyli
Copy link

leoyli commented Jun 30, 2019

I was wondering for this a while, nice job @thorn0!!!

@thynson
Copy link

thynson commented May 11, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants