Skip to content

Commit

Permalink
fix(typings): make lettable ofType correctly narrow action type
Browse files Browse the repository at this point in the history
- 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 redux-observable#382
  • Loading branch information
Hotell committed Jan 16, 2018
1 parent e4a4536 commit b1ed9fa
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 2 deletions.
67 changes: 67 additions & 0 deletions docs/Troubleshooting.md
Expand Up @@ -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) => {...})
)
```

* * *

Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Expand Up @@ -52,6 +52,6 @@ export declare function createEpicMiddleware<T extends Action, S, D = any>(rootE
export declare function combineEpics<T extends Action, S, D = any>(...epics: Epic<T, S, D>[]): Epic<T, S, D>;
export declare function combineEpics<E>(...epics: E[]): E;

export declare function ofType<T extends Action>(...keys: T['type'][]): (source: Observable<T>) => Observable<T>;
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';
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions test/typings.ts
Expand Up @@ -179,4 +179,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 b1ed9fa

Please sign in to comment.