Skip to content

Commit

Permalink
fix: improve TS types for createReducer, composeReducers and composeM…
Browse files Browse the repository at this point in the history
…iddleware
  • Loading branch information
jedwards1211 committed Nov 9, 2023
1 parent 75ca5e9 commit dcbe19f
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 15 deletions.
110 changes: 110 additions & 0 deletions src/composeMiddleware.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,116 @@ export function combineMiddlewareWithActionHandlers<
...middlewares: Middleware<DispatchExt, S, D>[]
): Middleware<DispatchExt, S, D>

export default function composeMiddleware<
Ext1 = {},
S = any,
D extends Dispatch = Dispatch
>(middleware1: Middleware<Ext1, S, D>): Middleware<Ext1, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>
): Middleware<Ext1 & Ext2, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
Ext3 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>,
middleware3: Middleware<Ext3, S, D>
): Middleware<Ext1 & Ext2 & Ext3, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
Ext3 = {},
Ext4 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>,
middleware3: Middleware<Ext3, S, D>,
middleware4: Middleware<Ext4, S, D>
): Middleware<Ext1 & Ext2 & Ext3 & Ext4, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
Ext3 = {},
Ext4 = {},
Ext5 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>,
middleware3: Middleware<Ext3, S, D>,
middleware4: Middleware<Ext4, S, D>,
middleware5: Middleware<Ext5, S, D>
): Middleware<Ext1 & Ext2 & Ext3 & Ext4 & Ext5, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
Ext3 = {},
Ext4 = {},
Ext5 = {},
Ext6 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>,
middleware3: Middleware<Ext3, S, D>,
middleware4: Middleware<Ext4, S, D>,
middleware5: Middleware<Ext5, S, D>,
middleware6: Middleware<Ext6, S, D>
): Middleware<Ext1 & Ext2 & Ext3 & Ext4 & Ext5 & Ext6, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
Ext3 = {},
Ext4 = {},
Ext5 = {},
Ext6 = {},
Ext7 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>,
middleware3: Middleware<Ext3, S, D>,
middleware4: Middleware<Ext4, S, D>,
middleware5: Middleware<Ext5, S, D>,
middleware6: Middleware<Ext6, S, D>,
middleware7: Middleware<Ext7, S, D>
): Middleware<Ext1 & Ext2 & Ext3 & Ext4 & Ext5 & Ext6 & Ext7, S, D>
export default function composeMiddleware<
Ext1 = {},
Ext2 = {},
Ext3 = {},
Ext4 = {},
Ext5 = {},
Ext6 = {},
Ext7 = {},
Ext8 = {},
S = any,
D extends Dispatch = Dispatch
>(
middleware1: Middleware<Ext1, S, D>,
middleware2: Middleware<Ext2, S, D>,
middleware3: Middleware<Ext3, S, D>,
middleware4: Middleware<Ext4, S, D>,
middleware5: Middleware<Ext5, S, D>,
middleware6: Middleware<Ext6, S, D>,
middleware7: Middleware<Ext7, S, D>,
middleware8: Middleware<Ext8, S, D>
): Middleware<Ext1 & Ext2 & Ext3 & Ext4 & Ext5 & Ext6 & Ext7 & Ext8, S, D>
export default function composeMiddleware<
DispatchExt = {},
S = any,
Expand Down
37 changes: 29 additions & 8 deletions src/composeReducers.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { AnyAction, Reducer } from 'redux'

type ReducerStateType<Reducer> = Reducer extends (
state: undefined | infer S0 extends {},
action: any
) => infer S1
? S0 | S1
: Reducer extends (state: infer S0 extends {}, action: any) => infer S1
? S0 | S1
: never

type ReducerActionType<Reducer> = Reducer extends (
state: any,
action: infer A extends AnyAction
) => any
? A
: never

export function combineReducersWithActionHandlers<
S = any,
A extends AnyAction = AnyAction
>(...reducers: Reducer<S, A>[]): Reducer<S, A>

export default function composeReducers<
S = any,
A extends AnyAction = AnyAction
>(...reducers: Reducer<S, A>[]): Reducer<S, A>[]
Reducers extends Reducer<any, any>[]
>(...reducers: Reducers): composeReducers<Reducers>

type composeReducers<Reducers extends Reducer<any, any>[]> = Reducer<
ReducerStateType<Reducers[number]>,
ReducerActionType<Reducers[number]>
>

declare function composeReducers<Reducers extends Reducer<any, any>[]>(
...reducers: Reducers
): composeReducers<Reducers>

export default composeReducers
43 changes: 37 additions & 6 deletions src/createReducer.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
import type { AnyAction, Reducer } from 'redux'

type HandlersStateType<
Handlers extends Record<string, (state: any, action: any) => any>
> = ReducerStateType<Handlers[keyof Handlers]>

export type ReducerStateType<Reducer> = Reducer extends (
state: infer S0,
action: any
) => infer S1
? Exclude<S0, undefined> | S1
: never

type HandlersActionType<
Handlers extends Record<string, (state: any, action: any) => any>
> = ReducerActionType<Handlers[keyof Handlers]>

export type ReducerActionType<Reducer> = Reducer extends (
state: any,
action: infer A extends AnyAction
) => any
? A
: never

type AnyCoalesce<T, U> = 0 extends 1 & T ? U : T

type ActionHandlers<S = any, A extends AnyAction = AnyAction> = {
[T in A['type']]?: (
state: S | undefined,
action: Extract<A, { type: T }>
) => S
}

type InitializedActionHandlers<S = any, A extends AnyAction = AnyAction> = {
[T in A['type']]?: (state: S, action: Extract<A, { type: T }>) => S
}

export default function createReducer<S = any, A extends AnyAction = AnyAction>(
initialState: S,
actionHandlers: Record<string, Reducer<S, A>>
actionHandlers: InitializedActionHandlers<S, A>
): Reducer<S, A>
export default function createReducer<S = any, A extends AnyAction = AnyAction>(
actionHandlers: Record<string, Reducer<S, A>>
): Reducer<S, A>
export default function createReducer<S = any, A extends AnyAction = AnyAction>(
initialState: S,
actionHandlers: Record<string, Reducer<S, A>>
actionHandlers: ActionHandlers<S, A>
): Reducer<S, A>
149 changes: 149 additions & 0 deletions test/typeTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Middleware, Reducer, applyMiddleware, createStore } from 'redux'
import { composeMiddleware, composeReducers, createReducer } from '../src'

export const LOGIN = 'login'
export const LOGOUT = 'logout'

export type LoginAction = {
type: typeof LOGIN
payload: {
password: string
}
}
export type LogoutAction = {
type: typeof LOGOUT
error?: true
payload?: {
error?: string
}
}

export type AuthAction = LoginAction | LogoutAction

export type Auth = {
error: string | undefined
}

export const reducer1: Reducer<Auth, AuthAction> = createReducer<
Auth,
AuthAction
>(
{ error: undefined },
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[LOGIN]: (state, action) => ({
error: undefined,
}),
[LOGOUT]: (state, { payload }) => ({
error: payload?.error,
}),
}
)

export const reducer2: Reducer<Auth, AuthAction> = createReducer<
Auth,
AuthAction
>({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[LOGIN]: (state, action) => ({
error: undefined,
}),
[LOGOUT]: (state, { payload }) => ({
error: payload?.error,
}),
})

export const reducer3: Reducer<Auth, AuthAction> = createReducer<
Auth,
AuthAction
>(
{ error: undefined },
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[LOGIN]: (state: Auth, action: LoginAction) => ({
error: undefined,
}),
[LOGOUT]: (state: Auth, { payload }: LogoutAction) => ({
error: payload?.error,
}),
}
)

export const reducer4: Reducer<Auth, AuthAction> = createReducer<
Auth,
AuthAction
>({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[LOGIN]: (state: Auth | undefined, action: LoginAction) => ({
error: undefined,
}),
[LOGOUT]: (state: Auth | undefined, { payload }: LogoutAction) => ({
error: payload?.error,
}),
})

export const reducer5 = composeReducers(
createReducer<Auth, LoginAction>({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[LOGIN]: (state: Auth | undefined, action: LoginAction) => ({
error: undefined,
}),
}),
createReducer<Auth, LogoutAction>({
[LOGOUT]: (state: Auth | undefined, { payload }: LogoutAction) => ({
error: payload?.error,
}),
})
)

export const reducer6: Reducer<Auth, AuthAction> = composeReducers(
createReducer<Auth, LoginAction>({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[LOGIN]: (state: Auth | undefined, action: LoginAction) => ({
error: undefined,
}),
}),
createReducer<Auth, LogoutAction>({
[LOGOUT]: (state: Auth | undefined, { payload }: LogoutAction) => ({
error: payload?.error,
}),
})
)

type AsyncAction<V> = {
type: 'async'
perform: () => Promise<V>
}

const asyncActionMiddleware: Middleware<
{
<V>(action: AsyncAction<V>): Promise<V>
},
Auth
> = (store) => (next) => (action) => null as any

type FooAction = {
type: 'foo'
}

const fooActionMiddleware: Middleware<
{
(action: FooAction): 'foo'
},
Auth
> = (store) => (next) => (action) => null as any

const store = applyMiddleware(
composeMiddleware(asyncActionMiddleware, fooActionMiddleware)
)(createStore)(reducer1)

const value1: 'foo' = store.dispatch({ type: 'foo' })
const value2: Promise<number> = store.dispatch({
type: 'async',
perform: async () => 3,
})
const value3: LoginAction = store.dispatch({
type: 'login',
payload: { password: 'foo' },
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "./node_modules/@jcoreio/toolchain-typescript/tsconfig.json",
"include": ["./src"],
"include": ["./src", "./test"],
"exclude": ["node_modules"],
"compilerOptions": {
"skipLibCheck": false
Expand Down

0 comments on commit dcbe19f

Please sign in to comment.