Skip to content

Commit

Permalink
fix(store): infer initial store state properly with metareducers (#3102)
Browse files Browse the repository at this point in the history
Closes #3007 

BREAKING CHANGES:

`initialState` needs to match the interface of the store/feature.

BEFORE:

Missing properties were valid

```ts
StoreModule.forRoot(reducers, {
  initialState: { notExisting: 3 },
  metaReducers: [metaReducer]
});
```

AFTER:

A type error is produced for initialState that does not match the store/feature

```ts
StoreModule.forRoot(reducers, {
  initialState: { notExisting: 3 },
  metaReducers: [metaReducer]
});
```
  • Loading branch information
timdeschryver authored Nov 2, 2021
1 parent 548c72c commit d003b85
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 2 deletions.
73 changes: 73 additions & 0 deletions modules/store/spec/types/store_module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expecter } from 'ts-snippet';
import { compilerOptions } from './utils';

describe('StoreModule', () => {
const expectSnippet = expecter(
(code) => `
import {StoreModule,ActionReducerMap,Action} from '@ngrx/store';
interface State {
featureA: object;
featureB: object;
}
const reducers: ActionReducerMap<State, Action> = {} as ActionReducerMap<
State,
Action
>;
function metaReducer(reducer) {
return (state, action) => {
return reducer(state, action);
};
}
${code}
`,
compilerOptions()
);

describe('StoreModule.forRoot()', () => {
it('accepts initialState and metaReducers with matching State', () => {
expectSnippet(`
StoreModule.forRoot(reducers, {
initialState: { featureA: {} },
metaReducers: [metaReducer]
});
`).toSucceed();
});

it("throws when initial state don't with store state", () => {
expectSnippet(`
StoreModule.forRoot(reducers, {
initialState: { notExisting: 3 },
metaReducers: [metaReducer]
});
`).toFail(
/Type '{ notExisting: number; }' is not assignable to type 'InitialState<State>/
);
});
});

describe('StoreModule.forFeature()', () => {
it('accepts initialState and metaReducers with matching State', () => {
expectSnippet(`
StoreModule.forFeature('feature', reducers, {
initialState: { featureA: {} },
metaReducers: [metaReducer]
});
`).toSucceed();
});

it("throws when initial state don't with store state", () => {
expectSnippet(`
StoreModule.forFeature('feature', reducers, {
initialState: { notExisting: 3 },
metaReducers: [metaReducer]
});
`).toFail(
/Type '{ notExisting: number; }' is not assignable to type 'InitialState<State>/
);
});
});
});
3 changes: 1 addition & 2 deletions modules/store/src/store_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
USER_PROVIDED_META_REDUCERS,
_RESOLVED_META_REDUCERS,
_ROOT_STORE_GUARD,
ACTIVE_RUNTIME_CHECKS,
_ACTION_TYPE_UNIQUENESS_CHECK,
} from './tokens';
import { ACTIONS_SUBJECT_PROVIDERS, ActionsSubject } from './actions_subject';
Expand Down Expand Up @@ -108,7 +107,7 @@ export class StoreFeatureModule implements OnDestroy {
export interface StoreConfig<T, V extends Action = Action> {
initialState?: InitialState<T>;
reducerFactory?: ActionReducerFactory<T, V>;
metaReducers?: MetaReducer<T, V>[];
metaReducers?: MetaReducer<{ [P in keyof T]: T[P] }, V>[];
}

export interface RootStoreConfig<T, V extends Action = Action>
Expand Down

0 comments on commit d003b85

Please sign in to comment.