Skip to content

Commit 0ac73b5

Browse files
Shakeskeyboardetimdorr
authored andcommitted
#2808 Preloaded state is now selectively partial (instead of deeply partial). (#3485)
* Preloaded state is now selectively partial (instead of deeply partial). * Improved CombinedState, PreloadedState, and removed UnCombinedState. Found a better way to type check CombinedState which allows the $CombinedState symbol property marker to be optional. Since it's optional, it's no longer necessary to strip it off in the Reducer state parameter type and return type. This leaves the type definition for Reducer unmodified, reduces the number of types required by one, and makes the resolved types and stack traces clearer. * Small change to the description of CombinedState. * Removed DeepPartial import from tests. Leaving the definition in place as removing it would be a breaking change. * Made prettier happy. * Made prettier happy with UsingObjectSpreadOperator.md
1 parent 63dda81 commit 0ac73b5

File tree

4 files changed

+79
-17
lines changed

4 files changed

+79
-17
lines changed

docs/recipes/UsingObjectSpreadOperator.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ While the object spread syntax is a [Stage 4](https://github.com/tc39/proposal-o
6464
"plugins": ["@babel/plugin-proposal-object-rest-spread"]
6565
}
6666
```
67+
6768
> ##### Note on Object Spread Operator
6869
69-
> Like the Array Spread Operator, the Object Spread Operator creates a [shallow clone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) of the original object. In other words, for multidimensional source objects, elements in the copied object at a depth greater than one are mere references to the source object (with the exception of [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), which are copied). Thus, you cannot reliably use the Object Spread Operator (`...`) for deep cloning objects.
70+
> Like the Array Spread Operator, the Object Spread Operator creates a [shallow clone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) of the original object. In other words, for multidimensional source objects, elements in the copied object at a depth greater than one are mere references to the source object (with the exception of [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), which are copied). Thus, you cannot reliably use the Object Spread Operator (`...`) for deep cloning objects.

index.d.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,46 @@ export interface AnyAction extends Action {
3232
[extraProps: string]: any
3333
}
3434

35+
/**
36+
* Internal "virtual" symbol used to make the `CombinedState` type unique.
37+
*/
38+
declare const $CombinedState: unique symbol
39+
40+
/**
41+
* State base type for reducers created with `combineReducers()`.
42+
*
43+
* This type allows the `createStore()` method to infer which levels of the
44+
* preloaded state can be partial.
45+
*
46+
* Because Typescript is really duck-typed, a type needs to have some
47+
* identifying property to differentiate it from other types with matching
48+
* prototypes for type checking purposes. That's why this type has the
49+
* `$CombinedState` symbol property. Without the property, this type would
50+
* match any object. The symbol doesn't really exist because it's an internal
51+
* (i.e. not exported), and internally we never check its value. Since it's a
52+
* symbol property, it's not expected to be unumerable, and the value is
53+
* typed as always undefined, so its never expected to have a meaningful
54+
* value anyway. It just makes this type distinquishable from plain `{}`.
55+
*/
56+
export type CombinedState<S> = { readonly [$CombinedState]?: undefined } & S
57+
58+
/**
59+
* Recursively makes combined state objects partial. Only combined state _root
60+
* objects_ (i.e. the generated higher level object with keys mapping to
61+
* individual reducers) are partial.
62+
*/
63+
export type PreloadedState<S> = Required<S> extends {
64+
[$CombinedState]: undefined
65+
}
66+
? S extends CombinedState<infer S1>
67+
? {
68+
[K in keyof S1]?: S1[K] extends object ? PreloadedState<S1[K]> : S1[K]
69+
}
70+
: never
71+
: {
72+
[K in keyof S]: S[K] extends object ? PreloadedState<S[K]> : S[K]
73+
}
74+
3575
/* reducers */
3676

3777
/**
@@ -136,13 +176,16 @@ export type ActionFromReducersMapObject<M> = M extends ReducersMapObject<
136176
*/
137177
export function combineReducers<S>(
138178
reducers: ReducersMapObject<S, any>
139-
): Reducer<S>
179+
): Reducer<CombinedState<S>>
140180
export function combineReducers<S, A extends Action = AnyAction>(
141181
reducers: ReducersMapObject<S, A>
142-
): Reducer<S, A>
182+
): Reducer<CombinedState<S>, A>
143183
export function combineReducers<M extends ReducersMapObject<any, any>>(
144184
reducers: M
145-
): Reducer<StateFromReducersMapObject<M>, ActionFromReducersMapObject<M>>
185+
): Reducer<
186+
CombinedState<StateFromReducersMapObject<M>>,
187+
ActionFromReducersMapObject<M>
188+
>
146189

147190
/* store */
148191

@@ -316,7 +359,7 @@ export interface StoreCreator {
316359
): Store<S & StateExt, A> & Ext
317360
<S, A extends Action, Ext, StateExt>(
318361
reducer: Reducer<S, A>,
319-
preloadedState?: DeepPartial<S>,
362+
preloadedState?: PreloadedState<S>,
320363
enhancer?: StoreEnhancer<Ext>
321364
): Store<S & StateExt, A> & Ext
322365
}
@@ -380,7 +423,7 @@ export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <
380423
A extends Action = AnyAction
381424
>(
382425
reducer: Reducer<S, A>,
383-
preloadedState?: DeepPartial<S>
426+
preloadedState?: PreloadedState<S>
384427
) => Store<S & StateExt, A> & Ext
385428

386429
/* middleware */

test/typescript/enhancers.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
import {
2-
StoreEnhancer,
3-
Action,
4-
AnyAction,
5-
Reducer,
6-
createStore,
7-
DeepPartial
8-
} from 'redux'
1+
import { PreloadedState } from '../../index'
2+
import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from 'redux'
93

104
interface State {
115
someField: 'string'
@@ -43,10 +37,10 @@ function stateExtension() {
4337
A extends Action = AnyAction
4438
>(
4539
reducer: Reducer<S, A>,
46-
preloadedState?: DeepPartial<S>
40+
preloadedState?: PreloadedState<S>
4741
) => {
4842
const wrappedReducer: Reducer<S & ExtraState, A> = null as any
49-
const wrappedPreloadedState: S & ExtraState = null as any
43+
const wrappedPreloadedState: PreloadedState<S & ExtraState> = null as any
5044
return createStore(wrappedReducer, wrappedPreloadedState)
5145
}
5246

test/typescript/store.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,45 @@ const funcWithStore = (store: Store<State, DerivedAction>) => {}
5757
const store: Store<State> = createStore(reducer)
5858

5959
const storeWithPreloadedState: Store<State> = createStore(reducer, {
60+
a: 'a',
61+
b: { c: 'c', d: 'd' }
62+
})
63+
// typings:expect-error
64+
const storeWithBadPreloadedState: Store<State> = createStore(reducer, {
6065
b: { c: 'c' }
6166
})
6267

6368
const storeWithActionReducer = createStore(reducerWithAction)
6469
const storeWithActionReducerAndPreloadedState = createStore(reducerWithAction, {
65-
b: { c: 'c' }
70+
a: 'a',
71+
b: { c: 'c', d: 'd' }
6672
})
6773
funcWithStore(storeWithActionReducer)
6874
funcWithStore(storeWithActionReducerAndPreloadedState)
6975

76+
// typings:expect-error
77+
const storeWithActionReducerAndBadPreloadedState = createStore(
78+
reducerWithAction,
79+
{
80+
b: { c: 'c' }
81+
}
82+
)
83+
7084
const enhancer: StoreEnhancer = next => next
7185

7286
const storeWithSpecificEnhancer: Store<State> = createStore(reducer, enhancer)
7387

7488
const storeWithPreloadedStateAndEnhancer: Store<State> = createStore(
89+
reducer,
90+
{
91+
a: 'a',
92+
b: { c: 'c', d: 'd' }
93+
},
94+
enhancer
95+
)
96+
97+
// typings:expect-error
98+
const storeWithBadPreloadedStateAndEnhancer: Store<State> = createStore(
7599
reducer,
76100
{
77101
b: { c: 'c' }

0 commit comments

Comments
 (0)