Skip to content
Permalink
Browse files

fix(typings): make lettable ofType correctly narrow action type (#385)

- docs: add typescript ofType troubleshooting solution
- chore(npm-scripts): enable all strict type-checking options for TS tests
- test(typings): cover ofType proper type narrowing
- docs: fix code typos
- fix(typings): make ofType definition non breaking

Closes #382
  • Loading branch information...
Hotell authored and jayphelps committed Jan 17, 2018
1 parent 716f6c1 commit 45d09a79783d2daba4391ca2a390d37155babd3d
Showing with 105 additions and 2 deletions.
  1. +67 −0 docs/Troubleshooting.md
  2. +1 −1 index.d.ts
  3. +1 −1 package.json
  4. +36 −0 test/typings.ts
@@ -125,6 +125,73 @@ const myEpic = action$ => {

This approach essentially returns an empty `Observable` from the epic, which does not cause any downstream actions.

### Typescript: ofType operator won't narrow to proper Observable type

Let's say you have following action types + action creator types:

```ts
import { Action } from 'redux'
const enum ActionTypes {
One = 'ACTION_ONE',
Two = 'ACTION_TWO',
}
const doOne = (myStr: string): One => ({type: ActionTypes.One, myStr})
const doTwo = (myBool: boolean): Two => ({type: ActionTypes.Two, myBool})
interface One extends Action {
type: ActionTypes.One
myStr: string
}
interface Two extends Action {
type: ActionTypes.Two
myBool: boolean
}
type Actions = One | Two
```
When you're using `.ofType` operator for filtering, returned observable won't be correctly narrowed within Type System, because its not capable of doing so yet ( TS 2.6.2 ).
To fix this, you need to explicitly set the generic type, so Typescript understands your intent, and narrows your action stream correctly:
```ts
// This will let action be `Actions` type, which is wrong
const epic = (action$: ActionsObservable<Actions>) =>
action$
.ofType(ActionTypes.One)
// action is still type Actions instead of One
.map((action) => {...})
// Explicitly set generics fixes the issue
const epic = (action$: ActionsObservable<Actions>) =>
action$
.ofType<One>(ActionTypes.One)
// action is correctly narrowed to One
.map((action) => {...})
```

Similar issue exists when lettable operators are used ( Rx >=5.5 ).

Again fix is similar by provide explicitly generics
> this time you need to provide both while epic stream + narrowed type
```ts
// With lettable operator, ofType won't narrow correctly
const epic = (action$: ActionsObservable<Actions>) =>
action$.pipe(
ofType(ActionTypes.One),
// action is still type Actions instead of One
map((action) => {...})
)
// Explicitly set generics fixes the issue
const epic = (action$: ActionsObservable<Actions>) =>
action$.pipe(
ofType<Actions,One>(ActionTypes.One),
// action is correctly narrowed to One
map((action) => {...})
)
```

* * *

@@ -51,6 +51,6 @@ export declare function createEpicMiddleware<T extends Action, S, D = any, O ext
export declare function combineEpics<T extends Action, S, D = any, O extends T = T>(...epics: Epic<T, S, D, O>[]): Epic<T, S, D, O>;
export declare function combineEpics<E>(...epics: E[]): E;

export declare function ofType<T extends Action, R extends T = T>(...keys: T['type'][]): (source: Observable<T>) => Observable<R>;
export declare function ofType<T extends Action, R extends T = T, K extends R['type'] = R['type']>(...key: K[]): (source: Observable<T>) => Observable<R>;

export declare const EPIC_END: '@@redux-observable/EPIC_END';
@@ -15,7 +15,7 @@
"clean": "rimraf lib temp dist",
"check": "npm run lint && npm run test",
"test": "npm run lint && npm run build && npm run build:tests && mocha temp && npm run test:typings",
"test:typings": "tsc --noImplicitAny index.d.ts test/typings.ts --outDir temp --target ES5 --moduleResolution node && cd temp && node typings.js",
"test:typings": "tsc --strict index.d.ts test/typings.ts --outDir temp --target ES5 --moduleResolution node && cd temp && node typings.js",
"shipit": "npm run clean && npm run build && npm run lint && npm test && scripts/preprepublish.sh && npm publish",
"docs:clean": "rimraf _book",
"docs:prepare": "gitbook install",
@@ -208,4 +208,40 @@ const action$1: ActionsObservable<FluxStandardAction> = new ActionsObservable<Fl
const action$2: ActionsObservable<FluxStandardAction> = ActionsObservable.of<FluxStandardAction>({ type: 'SECOND' }, { type: 'FIRST' }, asap);
const action$3: ActionsObservable<FluxStandardAction> = ActionsObservable.from<FluxStandardAction>([{ type: 'SECOND' }, { type: 'FIRST' }], asap);

{
// proper type narrowing
const enum ActionTypes {
One = 'ACTION_ONE',
Two = 'ACTION_TWO',
}
const doOne = (myStr: string): One => ({type: ActionTypes.One, myStr})
const doTwo = (myBool: boolean): Two => ({type: ActionTypes.Two, myBool})

interface One extends Action {
type: ActionTypes.One
myStr: string
}
interface Two extends Action {
type: ActionTypes.Two
myBool: boolean
}
type Actions = One | Two

// Explicitly set generics fixes the issue
const epic = (action$: ActionsObservable<Actions>) =>
action$
.ofType<One>(ActionTypes.One)
// action is correctly narrowed to One
.map((action) => { console.log(action.myStr) })

// Explicitly set generics fixes the issue
const epicLettable = (action$: ActionsObservable<Actions>) =>
action$.pipe(
ofType<Actions,One>(ActionTypes.One),
// action is correctly narrowed to One
map((action) => { console.log(action.myStr) })
);

}

console.log('typings.ts: OK');

0 comments on commit 45d09a7

Please sign in to comment.
You can’t perform that action at this time.