Skip to content

Commit cf02dd2

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(effects): improve types for ofType with action creators (#2175)
1 parent 46a8467 commit cf02dd2

File tree

5 files changed

+240
-62
lines changed

5 files changed

+240
-62
lines changed

modules/effects/spec/effect_creator.spec.ts

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,7 @@
11
import { of } from 'rxjs';
2-
import { expecter } from 'ts-snippet';
32
import { createEffect, getCreateEffectMetadata } from '../src/effect_creator';
43

54
describe('createEffect()', () => {
6-
describe('types', () => {
7-
const expectSnippet = expecter(
8-
code => `
9-
import { Action } from '@ngrx/store';
10-
import { createEffect } from '@ngrx/effects';
11-
import { of } from 'rxjs';
12-
${code}`,
13-
{
14-
moduleResolution: 'node',
15-
target: 'es2015',
16-
baseUrl: '.',
17-
experimentalDecorators: true,
18-
paths: {
19-
'@ngrx/store': ['./modules/store'],
20-
'@ngrx/effects': ['./modules/effects'],
21-
rxjs: ['../npm/node_modules/rxjs', './node_modules/rxjs'],
22-
},
23-
}
24-
);
25-
26-
describe('dispatch: true', () => {
27-
it('should enforce an Action return value', () => {
28-
expectSnippet(`
29-
const effect = createEffect(() => of({ type: 'a' }));
30-
`).toSucceed();
31-
32-
expectSnippet(`
33-
const effect = createEffect(() => of({ foo: 'a' }));
34-
`).toFail(
35-
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
36-
);
37-
});
38-
39-
it('should enforce an Action return value when dispatch is provided', () => {
40-
expectSnippet(`
41-
const effect = createEffect(() => of({ type: 'a' }), { dispatch: true });
42-
`).toSucceed();
43-
44-
expectSnippet(`
45-
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: true });
46-
`).toFail(
47-
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
48-
);
49-
});
50-
});
51-
52-
describe('dispatch: false', () => {
53-
it('should enforce an Observable return value', () => {
54-
expectSnippet(`
55-
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: false });
56-
`).toSucceed();
57-
58-
expectSnippet(`
59-
const effect = createEffect(() => ({ foo: 'a' }), { dispatch: false });
60-
`).toFail(
61-
/Type '{ foo: string; }' is not assignable to type 'Observable<unknown> | ((...args: any[]) => Observable<unknown>)'./
62-
);
63-
});
64-
});
65-
});
66-
675
it('should flag the variable with a meta tag', () => {
686
const effect = createEffect(() => of({ type: 'a' }));
697

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { expecter } from 'ts-snippet';
2+
import { compilerOptions } from './utils';
3+
4+
describe('createEffect()', () => {
5+
const expectSnippet = expecter(
6+
code => `
7+
import { Action } from '@ngrx/store';
8+
import { createEffect } from '@ngrx/effects';
9+
import { of } from 'rxjs';
10+
11+
${code}`,
12+
compilerOptions()
13+
);
14+
15+
describe('dispatch: true', () => {
16+
it('should enforce an Action return value', () => {
17+
expectSnippet(`
18+
const effect = createEffect(() => of({ type: 'a' }));
19+
`).toSucceed();
20+
21+
expectSnippet(`
22+
const effect = createEffect(() => of({ foo: 'a' }));
23+
`).toFail(
24+
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
25+
);
26+
});
27+
28+
it('should enforce an Action return value when dispatch is provided', () => {
29+
expectSnippet(`
30+
const effect = createEffect(() => of({ type: 'a' }), { dispatch: true });
31+
`).toSucceed();
32+
33+
expectSnippet(`
34+
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: true });
35+
`).toFail(
36+
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
37+
);
38+
});
39+
});
40+
41+
describe('dispatch: false', () => {
42+
it('should enforce an Observable return value', () => {
43+
expectSnippet(`
44+
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: false });
45+
`).toSucceed();
46+
47+
expectSnippet(`
48+
const effect = createEffect(() => ({ foo: 'a' }), { dispatch: false });
49+
`).toFail(
50+
/Type '{ foo: string; }' is not assignable to type 'Observable<unknown> | ((...args: any[]) => Observable<unknown>)'./
51+
);
52+
});
53+
});
54+
});
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { expecter } from 'ts-snippet';
2+
import { compilerOptions } from './utils';
3+
4+
describe('ofType()', () => {
5+
describe('action creators', () => {
6+
const expectSnippet = expecter(
7+
code => `
8+
import { Action, createAction, props } from '@ngrx/store';
9+
import { Actions, ofType } from '@ngrx/effects';
10+
import { of } from 'rxjs';
11+
12+
const actions$ = {} as Actions;
13+
14+
${code}`,
15+
compilerOptions()
16+
);
17+
18+
it('should infer correctly', () => {
19+
expectSnippet(`
20+
const actionA = createAction('Action A');
21+
const effect = actions$.pipe(ofType(actionA))
22+
`).toInfer('effect', 'Observable<TypedAction<"Action A">>');
23+
});
24+
25+
it('should infer correctly with props', () => {
26+
expectSnippet(`
27+
const actionA = createAction('Action A', props<{ foo: string }>()});
28+
const effect = actions$.pipe(ofType(actionA))
29+
`).toInfer(
30+
'effect',
31+
'Observable<{ foo: string; } & TypedAction<"Action A">>'
32+
);
33+
});
34+
35+
it('should infer correctly with function', () => {
36+
expectSnippet(`
37+
const actionA = createAction('Action A', (foo: string) => ({ foo }));
38+
const effect = actions$.pipe(ofType(actionA))
39+
`).toInfer(
40+
'effect',
41+
'Observable<{ foo: string; } & TypedAction<"Action A">>'
42+
);
43+
});
44+
45+
it('should infer correctly with multiple actions (with over 5 actions)', () => {
46+
expectSnippet(`
47+
const actionA = createAction('Action A');
48+
const actionB = createAction('Action B');
49+
const actionC = createAction('Action C');
50+
const actionD = createAction('Action D');
51+
const actionE = createAction('Action E');
52+
const actionF = createAction('Action F');
53+
const actionG = createAction('Action G');
54+
55+
const effect = actions$.pipe(ofType(actionA, actionB, actionC, actionD, actionE, actionF, actionG))
56+
`).toInfer(
57+
'effect',
58+
'Observable<TypedAction<"Action A"> | TypedAction<"Action B"> | TypedAction<"Action C"> | TypedAction<"Action D"> | TypedAction<"Action E"> | TypedAction<"Action F"> | TypedAction<"Action G">>'
59+
);
60+
});
61+
});
62+
63+
describe('strings with typed Actions', () => {
64+
const expectSnippet = expecter(
65+
code => `
66+
import { Action } from '@ngrx/store';
67+
import { Actions, ofType } from '@ngrx/effects';
68+
import { of } from 'rxjs';
69+
70+
const ACTION_A = 'ACTION A'
71+
const ACTION_B = 'ACTION B'
72+
const ACTION_C = 'ACTION C'
73+
const ACTION_D = 'ACTION D'
74+
const ACTION_E = 'ACTION E'
75+
const ACTION_F = 'ACTION F'
76+
77+
interface ActionA { type: typeof ACTION_A };
78+
interface ActionB { type: typeof ACTION_B };
79+
interface ActionC { type: typeof ACTION_C };
80+
interface ActionD { type: typeof ACTION_D };
81+
interface ActionE { type: typeof ACTION_E };
82+
interface ActionF { type: typeof ACTION_F };
83+
84+
${code}`,
85+
compilerOptions()
86+
);
87+
88+
it('should infer correctly', () => {
89+
expectSnippet(`
90+
const actions$ = {} as Actions<ActionA>;
91+
const effect = actions$.pipe(ofType(ACTION_A))
92+
`).toInfer('effect', 'Observable<ActionA>');
93+
});
94+
95+
it('should infer correctly with multiple actions (up to 5 actions)', () => {
96+
expectSnippet(`
97+
const actions$ = {} as Actions<ActionA | ActionB | ActionC | ActionD | ActionE>;
98+
const effect = actions$.pipe(ofType(ACTION_A, ACTION_B, ACTION_C, ACTION_D, ACTION_E))
99+
`).toInfer(
100+
'effect',
101+
'Observable<ActionA | ActionB | ActionC | ActionD | ActionE>'
102+
);
103+
});
104+
105+
it('should infer to Action when more than 5 actions', () => {
106+
expectSnippet(`
107+
const actions$ = {} as Actions<ActionA | ActionB | ActionC | ActionD | ActionE | ActionF>;
108+
const effect = actions$.pipe(ofType(ACTION_A, ACTION_B, ACTION_C, ACTION_D, ACTION_E, ACTION_F))
109+
`).toInfer('effect', 'Observable<Action>');
110+
});
111+
112+
it('should infer to never when the action is not in Actions', () => {
113+
expectSnippet(`
114+
const actions$ = {} as Actions<ActionA>;
115+
const effect = actions$.pipe(ofType(ACTION_B))
116+
`).toInfer('effect', 'Observable<never>');
117+
});
118+
});
119+
120+
describe('strings ofType generic', () => {
121+
const expectSnippet = expecter(
122+
code => `
123+
import { Action } from '@ngrx/store';
124+
import { Actions, ofType } from '@ngrx/effects';
125+
import { of } from 'rxjs';
126+
127+
const ACTION_A = 'ACTION A'
128+
const ACTION_B = 'ACTION B'
129+
const ACTION_C = 'ACTION C'
130+
const ACTION_D = 'ACTION D'
131+
const ACTION_E = 'ACTION E'
132+
const ACTION_F = 'ACTION F'
133+
134+
interface ActionA { type: typeof ACTION_A };
135+
interface ActionB { type: typeof ACTION_B };
136+
interface ActionC { type: typeof ACTION_C };
137+
interface ActionD { type: typeof ACTION_D };
138+
interface ActionE { type: typeof ACTION_E };
139+
interface ActionF { type: typeof ACTION_F };
140+
141+
${code}`,
142+
compilerOptions()
143+
);
144+
145+
it('should infer correctly', () => {
146+
expectSnippet(`
147+
const actions$ = {} as Actions;
148+
const effect = actions$.pipe(ofType<ActionA>(ACTION_A))
149+
`).toInfer('effect', 'Observable<ActionA>');
150+
});
151+
152+
it('should infer correctly with multiple actions (with over 5 actions)', () => {
153+
expectSnippet(`
154+
const actions$ = {} as Actions;
155+
const effect = actions$.pipe(ofType<ActionA | ActionB | ActionC | ActionD | ActionE | ActionF>(ACTION_A, ACTION_B, ACTION_C, ACTION_D, ACTION_E, ACTION_F))
156+
`).toInfer(
157+
'effect',
158+
'Observable<ActionA | ActionB | ActionC | ActionD | ActionE | ActionF>'
159+
);
160+
});
161+
162+
it('should infer to the generic even if the generic is wrong', () => {
163+
expectSnippet(`
164+
const actions$ = {} as Actions;
165+
const effect = actions$.pipe(ofType<ActionA>(ACTION_B))
166+
`).toInfer('effect', 'Observable<ActionA>');
167+
});
168+
});
169+
});

modules/effects/spec/types/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const compilerOptions = () => ({
2+
moduleResolution: 'node',
3+
target: 'es2015',
4+
baseUrl: '.',
5+
experimentalDecorators: true,
6+
paths: {
7+
'@ngrx/store': ['./modules/store'],
8+
'@ngrx/effects': ['./modules/effects'],
9+
rxjs: ['../npm/node_modules/rxjs', './node_modules/rxjs'],
10+
},
11+
});

modules/effects/src/actions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ type ActionExtractor<
5454
* 'Observable<never>'. In such cases one has to manually set the generic type
5555
* like `actions.ofType<AdditionAction>('add')`.
5656
*/
57+
export function ofType<
58+
AC extends ActionCreator<string, Creator>[],
59+
U extends Action = Action,
60+
V = ReturnType<AC[number]>
61+
>(...allowedTypes: AC): OperatorFunction<U, V>;
62+
5763
export function ofType<
5864
E extends Extract<U, { type: T1 }>,
5965
AC extends ActionCreator<string, Creator>,

0 commit comments

Comments
 (0)