Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

#2808 Preloaded state is now selectively partial (instead of deeply partial). #3485

Merged
merged 10 commits into from
Aug 12, 2019
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