Skip to content

Commit

Permalink
#2808 Preloaded state is now selectively partial (instead of deeply p…
Browse files Browse the repository at this point in the history
…artial). (#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
  • Loading branch information
Shakeskeyboarde authored and timdorr committed Aug 12, 2019
1 parent 63dda81 commit 0ac73b5
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 17 deletions.
3 changes: 2 additions & 1 deletion docs/recipes/UsingObjectSpreadOperator.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ While the object spread syntax is a [Stage 4](https://github.com/tc39/proposal-o
"plugins": ["@babel/plugin-proposal-object-rest-spread"]
}
```

> ##### Note on Object Spread Operator
> 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.
> 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.
53 changes: 48 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,46 @@ export interface AnyAction extends Action {
[extraProps: string]: any
}

/**
* Internal "virtual" symbol used to make the `CombinedState` type unique.
*/
declare const $CombinedState: unique symbol

/**
* State base type for reducers created with `combineReducers()`.
*
* This type allows the `createStore()` method to infer which levels of the
* preloaded state can be partial.
*
* Because Typescript is really duck-typed, a type needs to have some
* identifying property to differentiate it from other types with matching
* prototypes for type checking purposes. That's why this type has the
* `$CombinedState` symbol property. Without the property, this type would
* match any object. The symbol doesn't really exist because it's an internal
* (i.e. not exported), and internally we never check its value. Since it's a
* symbol property, it's not expected to be unumerable, and the value is
* typed as always undefined, so its never expected to have a meaningful
* value anyway. It just makes this type distinquishable from plain `{}`.
*/
export type CombinedState<S> = { readonly [$CombinedState]?: undefined } & S

/**
* Recursively makes combined state objects partial. Only combined state _root
* objects_ (i.e. the generated higher level object with keys mapping to
* individual reducers) are partial.
*/
export type PreloadedState<S> = Required<S> extends {
[$CombinedState]: undefined
}
? S extends CombinedState<infer S1>
? {
[K in keyof S1]?: S1[K] extends object ? PreloadedState<S1[K]> : S1[K]
}
: never
: {
[K in keyof S]: S[K] extends object ? PreloadedState<S[K]> : S[K]
}

/* reducers */

/**
Expand Down Expand Up @@ -136,13 +176,16 @@ export type ActionFromReducersMapObject<M> = M extends ReducersMapObject<
*/
export function combineReducers<S>(
reducers: ReducersMapObject<S, any>
): Reducer<S>
): Reducer<CombinedState<S>>
export function combineReducers<S, A extends Action = AnyAction>(
reducers: ReducersMapObject<S, A>
): Reducer<S, A>
): Reducer<CombinedState<S>, A>
export function combineReducers<M extends ReducersMapObject<any, any>>(
reducers: M
): Reducer<StateFromReducersMapObject<M>, ActionFromReducersMapObject<M>>
): Reducer<
CombinedState<StateFromReducersMapObject<M>>,
ActionFromReducersMapObject<M>
>

/* store */

Expand Down Expand Up @@ -316,7 +359,7 @@ export interface StoreCreator {
): Store<S & StateExt, A> & Ext
<S, A extends Action, Ext, StateExt>(
reducer: Reducer<S, A>,
preloadedState?: DeepPartial<S>,
preloadedState?: PreloadedState<S>,
enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext
}
Expand Down Expand Up @@ -380,7 +423,7 @@ export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <
A extends Action = AnyAction
>(
reducer: Reducer<S, A>,
preloadedState?: DeepPartial<S>
preloadedState?: PreloadedState<S>
) => Store<S & StateExt, A> & Ext

/* middleware */
Expand Down
14 changes: 4 additions & 10 deletions test/typescript/enhancers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import {
StoreEnhancer,
Action,
AnyAction,
Reducer,
createStore,
DeepPartial
} from 'redux'
import { PreloadedState } from '../../index'
import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from 'redux'

interface State {
someField: 'string'
Expand Down Expand Up @@ -43,10 +37,10 @@ function stateExtension() {
A extends Action = AnyAction
>(
reducer: Reducer<S, A>,
preloadedState?: DeepPartial<S>
preloadedState?: PreloadedState<S>
) => {
const wrappedReducer: Reducer<S & ExtraState, A> = null as any
const wrappedPreloadedState: S & ExtraState = null as any
const wrappedPreloadedState: PreloadedState<S & ExtraState> = null as any
return createStore(wrappedReducer, wrappedPreloadedState)
}

Expand Down
26 changes: 25 additions & 1 deletion test/typescript/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,45 @@ const funcWithStore = (store: Store<State, DerivedAction>) => {}
const store: Store<State> = createStore(reducer)

const storeWithPreloadedState: Store<State> = createStore(reducer, {
a: 'a',
b: { c: 'c', d: 'd' }
})
// typings:expect-error
const storeWithBadPreloadedState: Store<State> = createStore(reducer, {
b: { c: 'c' }
})

const storeWithActionReducer = createStore(reducerWithAction)
const storeWithActionReducerAndPreloadedState = createStore(reducerWithAction, {
b: { c: 'c' }
a: 'a',
b: { c: 'c', d: 'd' }
})
funcWithStore(storeWithActionReducer)
funcWithStore(storeWithActionReducerAndPreloadedState)

// typings:expect-error
const storeWithActionReducerAndBadPreloadedState = createStore(
reducerWithAction,
{
b: { c: 'c' }
}
)

const enhancer: StoreEnhancer = next => next

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

const storeWithPreloadedStateAndEnhancer: Store<State> = createStore(
reducer,
{
a: 'a',
b: { c: 'c', d: 'd' }
},
enhancer
)

// typings:expect-error
const storeWithBadPreloadedStateAndEnhancer: Store<State> = createStore(
reducer,
{
b: { c: 'c' }
Expand Down

0 comments on commit 0ac73b5

Please sign in to comment.