Skip to content
New issue

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

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

Already on GitHub? Sign in 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