Skip to content

Commit

Permalink
fix: reducers and effects with same name are correctly typed 4.3.X (#913
Browse files Browse the repository at this point in the history
)
  • Loading branch information
semoal committed Jul 7, 2021
1 parent 8ffa9c6 commit 3db2d9f
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 64 deletions.
31 changes: 24 additions & 7 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ import {
ActionCreator,
} from 'redux'

/**
* Utility type taken by type-fest repository
* https://github.com/sindresorhus/type-fest/blob/main/source/merge-exclusive.d.ts
* Merges Exclusively two types into one
* Used to fix this https://github.com/rematch/rematch/issues/912
*/
type Without<FirstType, SecondType> = {
[KeyType in Exclude<keyof FirstType, keyof SecondType>]: never
}
export type MergeExclusive<FirstType, SecondType> =
| FirstType
| SecondType extends object
?
| (Without<FirstType, SecondType> & SecondType)
| (Without<SecondType, FirstType> & FirstType)
: FirstType | SecondType
/**
* Custom Action interface, adds an additional field - `payload`.
*
Expand Down Expand Up @@ -333,13 +349,14 @@ export type ExtractRematchDispatchersFromModels<
export type ModelDispatcher<
TModel extends Model<TModels>,
TModels extends Models<TModels>
> = ExtractRematchDispatchersFromReducers<
TModel['state'],
TModel['reducers'],
TModels
> &
ExtractRematchDispatchersFromEffects<TModel['effects'], TModels>

> = MergeExclusive<
ExtractRematchDispatchersFromEffects<TModel['effects'], TModels>,
ExtractRematchDispatchersFromReducers<
TModel['state'],
TModel['reducers'],
TModels
>
>
/** ************************ Reducers Dispatcher ************************* */

/**
Expand Down
26 changes: 16 additions & 10 deletions packages/core/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Middleware } from 'redux'
import { init } from '../src'
import { createModel, init, Models } from '../src'

describe('init config:', () => {
test('should not throw with an empty config', () => {
Expand Down Expand Up @@ -29,17 +29,23 @@ describe('init config:', () => {
return next(newAction)
}

const store = init({
models: {
count: {
state: 0,
reducers: {
addBy(state: number, payload: number): number {
return state + payload
},
},
const count = createModel<RootModel>()({
state: 0,
reducers: {
addBy(state: number, payload: number): number {
return state + payload
},
},
})

interface RootModel extends Models<RootModel> {
count: typeof count
}

const store = init<RootModel>({
models: {
count,
},
redux: {
middlewares: [add5Middleware, subtract2Middleware],
},
Expand Down
100 changes: 79 additions & 21 deletions packages/core/test/dispatcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { init, ModelDispatcher, Models } from '../src'
import { createModel, init, ModelDispatcher, Models } from '../src'

describe('dispatch:', () => {
describe('action:', () => {
Expand Down Expand Up @@ -47,12 +47,15 @@ describe('dispatch:', () => {
it('should dispatch an action', () => {
type CountState = number

const count = {
interface RootModel extends Models<RootModel> {
count: typeof count
}
const count = createModel<RootModel>()({
state: 0,
reducers: {
add: (state: CountState): CountState => state + 1,
},
}
})

const store = init({
models: { count },
Expand All @@ -69,12 +72,15 @@ describe('dispatch:', () => {
it('should dispatch multiple actions', () => {
type CountState = number

const count = {
interface RootModel extends Models<RootModel> {
count: typeof count
}
const count = createModel<RootModel>()({
state: 0,
reducers: {
add: (state: CountState): CountState => state + 1,
},
}
})

const store = init({
models: { count },
Expand All @@ -91,21 +97,26 @@ describe('dispatch:', () => {
it('should handle multiple models', () => {
type CountState = number

const a = {
const a = createModel<RootModel>()({
state: 42,
reducers: {
add: (state: CountState): CountState => state + 1,
},
}
})

const b = {
const b = createModel<RootModel>()({
state: 0,
reducers: {
add: (state: CountState): CountState => state + 1,
},
})

interface RootModel extends Models<RootModel> {
a: typeof a
b: typeof b
}

const store = init({
const store = init<RootModel>({
models: { a, b },
})

Expand All @@ -124,7 +135,7 @@ describe('dispatch:', () => {
meta: any
}

const count = {
const count = createModel<RootModel>()({
state: {
count: 0,
meta: null,
Expand All @@ -141,9 +152,13 @@ describe('dispatch:', () => {
}
},
},
})

interface RootModel extends Models<RootModel> {
count: typeof count
}

const store = init({
const store = init<RootModel>({
models: { count },
})

Expand All @@ -154,25 +169,60 @@ describe('dispatch:', () => {
meta: { some_meta: true },
})
})

it('effects functions that share a name with a reducer are called after their reducer counterpart.', () => {
interface RootModel extends Models<RootModel> {
count: typeof count
}

const effectMock = jest.fn()
const reducerMock = jest.fn()

const count = createModel<RootModel>()({
state: {
count: 0,
},
reducers: {
add(state) {
reducerMock()
return state
},
},
effects: () => ({
add() {
effectMock()
},
}),
})

const store = init<RootModel>({
models: { count },
})

store.dispatch.count.add()
expect(effectMock.mock.invocationCallOrder[0]).toBeGreaterThan(
reducerMock.mock.invocationCallOrder[0]
)
})
})

it('should include a payload if it is a false value', () => {
type AState = boolean

const a = {
const a = createModel<RootModel>()({
state: true,
reducers: {
toggle: (_: AState, payload: boolean): boolean => payload,
},
}
})

type RootModel = {
const models: RootModel = { a }

interface RootModel extends Models<RootModel> {
a: typeof a
}

const models: RootModel = { a }

const store = init({
const store = init<RootModel>({
models,
})

Expand Down Expand Up @@ -215,14 +265,18 @@ describe('dispatch:', () => {
it('should pass state as the first reducer param', () => {
type CountState = number

const count = {
const count = createModel<RootModel>()({
state: 0,
reducers: {
doNothing: (state: CountState): CountState => state,
},
})

interface RootModel extends Models<RootModel> {
count: typeof count
}

const store = init({
const store = init<RootModel>({
models: { count },
})

Expand All @@ -238,7 +292,7 @@ describe('dispatch:', () => {
countIds: number[]
}

const count = {
const count = createModel<RootModel>()({
state: {
countIds: [],
} as CountState,
Expand All @@ -250,9 +304,13 @@ describe('dispatch:', () => {
}
},
},
})

interface RootModel extends Models<RootModel> {
count: typeof count
}

const store = init({
const store = init<RootModel>({
models: { count },
})

Expand Down
12 changes: 6 additions & 6 deletions packages/core/test/listener.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { init } from '../src'
import { createModel, init, Models } from '../src'

describe('listener:', () => {
test('should trigger state changes on another models reducers', () => {
type CountState = number

const count1 = {
const count1 = createModel<RootModel>()({
state: 0,
reducers: {
increment: (state: CountState, payload: number): CountState =>
state + payload,
},
}
})

const count2 = {
const count2 = createModel<RootModel>()({
state: 0,
reducers: {
'count1/increment': (state: CountState, payload: number): CountState =>
state + payload,
},
}
})

type RootModel = {
interface RootModel extends Models<RootModel> {
count1: typeof count1
count2: typeof count2
}
Expand Down
25 changes: 15 additions & 10 deletions packages/core/test/multiple.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { init } from '../src'
import { createModel, init, Models } from '../src'

describe('multiple stores:', () => {
afterEach(() => {
Expand All @@ -19,11 +19,15 @@ describe('multiple stores:', () => {
})

test('should be able to store.dispatch to specific stores', () => {
const count = {
const count = createModel<RootModel>()({
state: 0,
reducers: {
increment: (state: number): number => state + 1,
},
})

interface RootModel extends Models<RootModel> {
count: typeof count
}

const store1 = init({ models: { count } })
Expand Down Expand Up @@ -64,27 +68,28 @@ describe('multiple stores:', () => {
})

test('dispatch should not contain another stores reducers', () => {
const count1 = {
const count1 = createModel<RootModel>()({
state: 0,
reducers: {
increment: (state: number): number => state + 1,
},
}
})

const count2 = {
const count2 = createModel<RootModel>()({
state: 0,
reducers: {
add: (state: number): number => state + 2,
},
}
})

const store1 = init({ models: { count: count1 } })
const store2 = init({ models: { count: count2 } })
interface RootModel extends Models<RootModel> {
count: typeof count1 | typeof count2
}
const store1 = init<RootModel>({ models: { count: count1 } })
const store2 = init<RootModel>({ models: { count: count2 } })

expect(store1.dispatch.count.increment).toBeDefined()
// @ts-expect-error
expect(store2.dispatch.count.increment).not.toBeDefined()
// @ts-expect-error
expect(store1.dispatch.count.add).not.toBeDefined()
expect(store2.dispatch.count.add).toBeDefined()
})
Expand Down

0 comments on commit 3db2d9f

Please sign in to comment.