Skip to content

Commit 21600bd

Browse files
committed
Initial implementation of withTypes
1 parent ce2bf4d commit 21600bd

File tree

6 files changed

+106
-23
lines changed

6 files changed

+106
-23
lines changed

src/hooks/useDispatch.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
import type { Action, Dispatch, UnknownAction } from 'redux'
21
import type { Context } from 'react'
2+
import type { Action, Dispatch, UnknownAction } from 'redux'
33

44
import type { ReactReduxContextValue } from '../components/Context'
55
import { ReactReduxContext } from '../components/Context'
6-
import { useStore as useDefaultStore, createStoreHook } from './useStore'
6+
import { createStoreHook, useStore as useDefaultStore } from './useStore'
7+
8+
export interface UseDispatch {
9+
<
10+
AppDispatch extends Dispatch<Action> = Dispatch<UnknownAction>
11+
>(): AppDispatch
12+
withTypes: <AppDispatch extends Dispatch<Action>>() => () => AppDispatch
13+
}
14+
// export interface UseDispatch<
15+
// DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>
16+
// > {
17+
// <AppDispatch extends DispatchType = DispatchType>(): AppDispatch
18+
// withTypes: <
19+
// OverrideDispatchType extends DispatchType
20+
// >() => UseDispatch<OverrideDispatchType>
21+
// }
722

823
/**
924
* Hook factory, which creates a `useDispatch` hook bound to a given context.
@@ -20,13 +35,19 @@ export function createDispatchHook<
2035
// @ts-ignore
2136
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
2237

23-
return function useDispatch<
38+
const useDispatch = <
2439
AppDispatch extends Dispatch<A> = Dispatch<A>
25-
>(): AppDispatch {
40+
>(): AppDispatch => {
2641
const store = useStore()
2742
// @ts-ignore
2843
return store.dispatch
2944
}
45+
46+
Object.assign(useDispatch, {
47+
withTypes: () => useDispatch,
48+
})
49+
50+
return useDispatch as UseDispatch
3051
}
3152

3253
/**

src/hooks/useSelector.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { React } from '../utils/react'
33

44
import type { ReactReduxContextValue } from '../components/Context'
55
import { ReactReduxContext } from '../components/Context'
6-
import type { EqualityFn, NoInfer } from '../types'
6+
import type { EqualityFn, NoInfer, TypedUseSelectorHook } from '../types'
77
import type { uSESWS } from '../utils/useSyncExternalStore'
88
import { notInitialized } from '../utils/useSyncExternalStore'
99
import {
@@ -75,7 +75,21 @@ export interface UseSelector {
7575
selector: (state: TState) => Selected,
7676
options?: UseSelectorOptions<Selected>
7777
): Selected
78+
withTypes: <TState>() => TypedUseSelectorHook<TState>
7879
}
80+
// export interface UseSelector<StateType = unknown> {
81+
// <TState extends StateType = StateType, Selected = unknown>(
82+
// selector: (state: TState) => Selected,
83+
// equalityFn?: EqualityFn<Selected>
84+
// ): Selected
85+
// <TState extends StateType = StateType, Selected = unknown>(
86+
// selector: (state: TState) => Selected,
87+
// options?: UseSelectorOptions<Selected>
88+
// ): Selected
89+
// withTypes: <
90+
// OverrideStateType extends StateType
91+
// >() => UseSelector<OverrideStateType>
92+
// }
7993

8094
let useSyncExternalStoreWithSelector = notInitialized as uSESWS
8195
export const initializeUseSelector = (fn: uSESWS) => {
@@ -101,12 +115,12 @@ export function createSelectorHook(
101115
? useDefaultReduxContext
102116
: createReduxContextHook(context)
103117

104-
return function useSelector<TState, Selected extends unknown>(
118+
const useSelector = <TState, Selected extends unknown>(
105119
selector: (state: TState) => Selected,
106120
equalityFnOrOptions:
107121
| EqualityFn<NoInfer<Selected>>
108122
| UseSelectorOptions<NoInfer<Selected>> = {}
109-
): Selected {
123+
): Selected => {
110124
const { equalityFn = refEquality, devModeChecks = {} } =
111125
typeof equalityFnOrOptions === 'function'
112126
? { equalityFn: equalityFnOrOptions }
@@ -215,6 +229,12 @@ export function createSelectorHook(
215229

216230
return selectedState
217231
}
232+
233+
Object.assign(useSelector, {
234+
withTypes: () => useSelector,
235+
})
236+
237+
return useSelector as UseSelector
218238
}
219239

220240
/**

src/hooks/useStore.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
11
import type { Context } from 'react'
2-
import type { Action as BasicAction, UnknownAction, Store } from 'redux'
2+
import type { Action as BasicAction, Store, UnknownAction } from 'redux'
33
import type { ReactReduxContextValue } from '../components/Context'
44
import { ReactReduxContext } from '../components/Context'
55
import {
6-
useReduxContext as useDefaultReduxContext,
76
createReduxContextHook,
7+
useReduxContext as useDefaultReduxContext,
88
} from './useReduxContext'
99

10+
export type StoreAction<StoreType extends Store> = StoreType extends Store<
11+
any,
12+
infer Action
13+
>
14+
? Action
15+
: never
16+
17+
export interface UseStore {
18+
<State = any, Action extends BasicAction = UnknownAction>(): Store<
19+
State,
20+
Action
21+
>
22+
23+
withTypes: <AppStore extends Store>() => () => AppStore
24+
}
25+
// export interface UseStore<StoreType extends Store = Store> {
26+
// <
27+
// State extends ReturnType<StoreType['getState']> = ReturnType<
28+
// StoreType['getState']
29+
// >,
30+
// Action extends BasicAction = StoreAction<Store>
31+
// >(): Store<State, Action>
32+
33+
// withTypes: <
34+
// OverrideStoreType extends StoreType
35+
// >() => UseStore<OverrideStoreType>
36+
// }
37+
1038
/**
1139
* Hook factory, which creates a `useStore` hook bound to a given context.
1240
*
@@ -15,7 +43,7 @@ import {
1543
*/
1644
export function createStoreHook<
1745
S = unknown,
18-
A extends BasicAction = UnknownAction
46+
A extends BasicAction = BasicAction
1947
// @ts-ignore
2048
>(context?: Context<ReactReduxContextValue<S, A> | null> = ReactReduxContext) {
2149
const useReduxContext =
@@ -24,15 +52,21 @@ export function createStoreHook<
2452
? useDefaultReduxContext
2553
: // @ts-ignore
2654
createReduxContextHook(context)
27-
return function useStore<
55+
const useStore = <
2856
State = S,
2957
Action2 extends BasicAction = A
3058
// @ts-ignore
31-
>() {
59+
>() => {
3260
const { store } = useReduxContext()
3361
// @ts-ignore
3462
return store as Store<State, Action2>
3563
}
64+
65+
Object.assign(useStore, {
66+
withTypes: () => useStore,
67+
})
68+
69+
return useStore as UseStore
3670
}
3771

3872
/**

test/hooks/useSelector.spec.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,12 @@ describe('React', () => {
10551055
})
10561056
})
10571057
})
1058+
1059+
test('useSelector.withTypes', () => {
1060+
const useAppSelector = useSelector.withTypes<RootState>()
1061+
1062+
expect(useAppSelector).toBe(useSelector)
1063+
})
10581064
})
10591065

10601066
describe('createSelectorHook', () => {

test/typetests/counterApp.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import {
2-
createSlice,
3-
createAsyncThunk,
4-
configureStore,
5-
ThunkAction,
6-
Action,
7-
} from '@reduxjs/toolkit'
1+
import type { Action, ThunkAction } from '@reduxjs/toolkit'
2+
import { configureStore, createAsyncThunk, createSlice } from '@reduxjs/toolkit'
83

94
export interface CounterState {
105
counter: number
@@ -46,6 +41,7 @@ const counterStore = configureStore({
4641
middleware: (gdm) => gdm(),
4742
})
4843

44+
export type AppStore = typeof counterStore
4945
export type AppDispatch = typeof counterStore.dispatch
5046
export type RootState = ReturnType<typeof counterStore.getState>
5147
export type AppThunk<ReturnType = void> = ThunkAction<

test/typetests/hooks.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@ import {
1818
useStore,
1919
} from '../../src/index'
2020

21-
import type { AppDispatch, RootState } from './counterApp'
21+
import type { AppDispatch, AppStore, RootState } from './counterApp'
2222
import { incrementAsync } from './counterApp'
2323

2424
import { expectExactType, expectType } from '../typeTestHelpers'
2525

2626
function preTypedHooksSetup() {
2727
// Standard hooks setup
28-
const useAppDispatch = () => useDispatch<AppDispatch>()
29-
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
28+
const useAppDispatch = useDispatch.withTypes<AppDispatch>()
29+
// const useAppDispatch = () => useDispatch<AppDispatch>()
30+
// const useAppSelector: UseSelector<RootState> = useSelector
31+
const useAppSelector = useSelector.withTypes<RootState>()
32+
33+
useAppSelector((state) => state.counter)
34+
const useAppStore = useStore.withTypes<AppStore>()
3035

3136
function CounterComponent() {
3237
const dispatch = useAppDispatch()
@@ -84,7 +89,8 @@ function testShallowEqual() {
8489
)
8590
expectExactType<string>(selected1)
8691

87-
const useAppSelector: TypedUseSelectorHook<TestState> = useSelector
92+
// const useAppSelector: UseSelector<TestState> = useSelector
93+
const useAppSelector = useSelector.withTypes<TestState>()
8894

8995
const selected2 = useAppSelector((state) => state.stateProp, shallowEqual)
9096
expectExactType<string>(selected2)

0 commit comments

Comments
 (0)