Skip to content

Commit

Permalink
feat(effects): improve types for ofType with action creators (#2175)
Browse files Browse the repository at this point in the history
  • Loading branch information
timdeschryver authored and brandonroberts committed Oct 28, 2019
1 parent 46a8467 commit cf02dd2
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 62 deletions.
62 changes: 0 additions & 62 deletions modules/effects/spec/effect_creator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,7 @@
import { of } from 'rxjs';
import { expecter } from 'ts-snippet';
import { createEffect, getCreateEffectMetadata } from '../src/effect_creator';

describe('createEffect()', () => {
describe('types', () => {
const expectSnippet = expecter(
code => `
import { Action } from '@ngrx/store';
import { createEffect } from '@ngrx/effects';
import { of } from 'rxjs';
${code}`,
{
moduleResolution: 'node',
target: 'es2015',
baseUrl: '.',
experimentalDecorators: true,
paths: {
'@ngrx/store': ['./modules/store'],
'@ngrx/effects': ['./modules/effects'],
rxjs: ['../npm/node_modules/rxjs', './node_modules/rxjs'],
},
}
);

describe('dispatch: true', () => {
it('should enforce an Action return value', () => {
expectSnippet(`
const effect = createEffect(() => of({ type: 'a' }));
`).toSucceed();

expectSnippet(`
const effect = createEffect(() => of({ foo: 'a' }));
`).toFail(
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
);
});

it('should enforce an Action return value when dispatch is provided', () => {
expectSnippet(`
const effect = createEffect(() => of({ type: 'a' }), { dispatch: true });
`).toSucceed();

expectSnippet(`
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: true });
`).toFail(
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
);
});
});

describe('dispatch: false', () => {
it('should enforce an Observable return value', () => {
expectSnippet(`
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: false });
`).toSucceed();

expectSnippet(`
const effect = createEffect(() => ({ foo: 'a' }), { dispatch: false });
`).toFail(
/Type '{ foo: string; }' is not assignable to type 'Observable<unknown> | ((...args: any[]) => Observable<unknown>)'./
);
});
});
});

it('should flag the variable with a meta tag', () => {
const effect = createEffect(() => of({ type: 'a' }));

Expand Down
54 changes: 54 additions & 0 deletions modules/effects/spec/types/effect_creator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expecter } from 'ts-snippet';
import { compilerOptions } from './utils';

describe('createEffect()', () => {
const expectSnippet = expecter(
code => `
import { Action } from '@ngrx/store';
import { createEffect } from '@ngrx/effects';
import { of } from 'rxjs';
${code}`,
compilerOptions()
);

describe('dispatch: true', () => {
it('should enforce an Action return value', () => {
expectSnippet(`
const effect = createEffect(() => of({ type: 'a' }));
`).toSucceed();

expectSnippet(`
const effect = createEffect(() => of({ foo: 'a' }));
`).toFail(
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
);
});

it('should enforce an Action return value when dispatch is provided', () => {
expectSnippet(`
const effect = createEffect(() => of({ type: 'a' }), { dispatch: true });
`).toSucceed();

expectSnippet(`
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: true });
`).toFail(
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
);
});
});

describe('dispatch: false', () => {
it('should enforce an Observable return value', () => {
expectSnippet(`
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: false });
`).toSucceed();

expectSnippet(`
const effect = createEffect(() => ({ foo: 'a' }), { dispatch: false });
`).toFail(
/Type '{ foo: string; }' is not assignable to type 'Observable<unknown> | ((...args: any[]) => Observable<unknown>)'./
);
});
});
});
169 changes: 169 additions & 0 deletions modules/effects/spec/types/of_type.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { expecter } from 'ts-snippet';
import { compilerOptions } from './utils';

describe('ofType()', () => {
describe('action creators', () => {
const expectSnippet = expecter(
code => `
import { Action, createAction, props } from '@ngrx/store';
import { Actions, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
const actions$ = {} as Actions;
${code}`,
compilerOptions()
);

it('should infer correctly', () => {
expectSnippet(`
const actionA = createAction('Action A');
const effect = actions$.pipe(ofType(actionA))
`).toInfer('effect', 'Observable<TypedAction<"Action A">>');
});

it('should infer correctly with props', () => {
expectSnippet(`
const actionA = createAction('Action A', props<{ foo: string }>()});
const effect = actions$.pipe(ofType(actionA))
`).toInfer(
'effect',
'Observable<{ foo: string; } & TypedAction<"Action A">>'
);
});

it('should infer correctly with function', () => {
expectSnippet(`
const actionA = createAction('Action A', (foo: string) => ({ foo }));
const effect = actions$.pipe(ofType(actionA))
`).toInfer(
'effect',
'Observable<{ foo: string; } & TypedAction<"Action A">>'
);
});

it('should infer correctly with multiple actions (with over 5 actions)', () => {
expectSnippet(`
const actionA = createAction('Action A');
const actionB = createAction('Action B');
const actionC = createAction('Action C');
const actionD = createAction('Action D');
const actionE = createAction('Action E');
const actionF = createAction('Action F');
const actionG = createAction('Action G');
const effect = actions$.pipe(ofType(actionA, actionB, actionC, actionD, actionE, actionF, actionG))
`).toInfer(
'effect',
'Observable<TypedAction<"Action A"> | TypedAction<"Action B"> | TypedAction<"Action C"> | TypedAction<"Action D"> | TypedAction<"Action E"> | TypedAction<"Action F"> | TypedAction<"Action G">>'
);
});
});

describe('strings with typed Actions', () => {
const expectSnippet = expecter(
code => `
import { Action } from '@ngrx/store';
import { Actions, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
const ACTION_A = 'ACTION A'
const ACTION_B = 'ACTION B'
const ACTION_C = 'ACTION C'
const ACTION_D = 'ACTION D'
const ACTION_E = 'ACTION E'
const ACTION_F = 'ACTION F'
interface ActionA { type: typeof ACTION_A };
interface ActionB { type: typeof ACTION_B };
interface ActionC { type: typeof ACTION_C };
interface ActionD { type: typeof ACTION_D };
interface ActionE { type: typeof ACTION_E };
interface ActionF { type: typeof ACTION_F };
${code}`,
compilerOptions()
);

it('should infer correctly', () => {
expectSnippet(`
const actions$ = {} as Actions<ActionA>;
const effect = actions$.pipe(ofType(ACTION_A))
`).toInfer('effect', 'Observable<ActionA>');
});

it('should infer correctly with multiple actions (up to 5 actions)', () => {
expectSnippet(`
const actions$ = {} as Actions<ActionA | ActionB | ActionC | ActionD | ActionE>;
const effect = actions$.pipe(ofType(ACTION_A, ACTION_B, ACTION_C, ACTION_D, ACTION_E))
`).toInfer(
'effect',
'Observable<ActionA | ActionB | ActionC | ActionD | ActionE>'
);
});

it('should infer to Action when more than 5 actions', () => {
expectSnippet(`
const actions$ = {} as Actions<ActionA | ActionB | ActionC | ActionD | ActionE | ActionF>;
const effect = actions$.pipe(ofType(ACTION_A, ACTION_B, ACTION_C, ACTION_D, ACTION_E, ACTION_F))
`).toInfer('effect', 'Observable<Action>');
});

it('should infer to never when the action is not in Actions', () => {
expectSnippet(`
const actions$ = {} as Actions<ActionA>;
const effect = actions$.pipe(ofType(ACTION_B))
`).toInfer('effect', 'Observable<never>');
});
});

describe('strings ofType generic', () => {
const expectSnippet = expecter(
code => `
import { Action } from '@ngrx/store';
import { Actions, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
const ACTION_A = 'ACTION A'
const ACTION_B = 'ACTION B'
const ACTION_C = 'ACTION C'
const ACTION_D = 'ACTION D'
const ACTION_E = 'ACTION E'
const ACTION_F = 'ACTION F'
interface ActionA { type: typeof ACTION_A };
interface ActionB { type: typeof ACTION_B };
interface ActionC { type: typeof ACTION_C };
interface ActionD { type: typeof ACTION_D };
interface ActionE { type: typeof ACTION_E };
interface ActionF { type: typeof ACTION_F };
${code}`,
compilerOptions()
);

it('should infer correctly', () => {
expectSnippet(`
const actions$ = {} as Actions;
const effect = actions$.pipe(ofType<ActionA>(ACTION_A))
`).toInfer('effect', 'Observable<ActionA>');
});

it('should infer correctly with multiple actions (with over 5 actions)', () => {
expectSnippet(`
const actions$ = {} as Actions;
const effect = actions$.pipe(ofType<ActionA | ActionB | ActionC | ActionD | ActionE | ActionF>(ACTION_A, ACTION_B, ACTION_C, ACTION_D, ACTION_E, ACTION_F))
`).toInfer(
'effect',
'Observable<ActionA | ActionB | ActionC | ActionD | ActionE | ActionF>'
);
});

it('should infer to the generic even if the generic is wrong', () => {
expectSnippet(`
const actions$ = {} as Actions;
const effect = actions$.pipe(ofType<ActionA>(ACTION_B))
`).toInfer('effect', 'Observable<ActionA>');
});
});
});
11 changes: 11 additions & 0 deletions modules/effects/spec/types/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const compilerOptions = () => ({
moduleResolution: 'node',
target: 'es2015',
baseUrl: '.',
experimentalDecorators: true,
paths: {
'@ngrx/store': ['./modules/store'],
'@ngrx/effects': ['./modules/effects'],
rxjs: ['../npm/node_modules/rxjs', './node_modules/rxjs'],
},
});
6 changes: 6 additions & 0 deletions modules/effects/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ type ActionExtractor<
* 'Observable<never>'. In such cases one has to manually set the generic type
* like `actions.ofType<AdditionAction>('add')`.
*/
export function ofType<
AC extends ActionCreator<string, Creator>[],
U extends Action = Action,
V = ReturnType<AC[number]>
>(...allowedTypes: AC): OperatorFunction<U, V>;

export function ofType<
E extends Extract<U, { type: T1 }>,
AC extends ActionCreator<string, Creator>,
Expand Down

0 comments on commit cf02dd2

Please sign in to comment.