diff --git a/docs/recipes/UsingObjectSpreadOperator.md b/docs/recipes/UsingObjectSpreadOperator.md index cb129f3315..fa860b867a 100644 --- a/docs/recipes/UsingObjectSpreadOperator.md +++ b/docs/recipes/UsingObjectSpreadOperator.md @@ -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. diff --git a/index.d.ts b/index.d.ts index c9c5a57d75..50e8d018fa 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 = { 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 = Required extends { + [$CombinedState]: undefined +} + ? S extends CombinedState + ? { + [K in keyof S1]?: S1[K] extends object ? PreloadedState : S1[K] + } + : never + : { + [K in keyof S]: S[K] extends object ? PreloadedState : S[K] + } + /* reducers */ /** @@ -136,13 +176,16 @@ export type ActionFromReducersMapObject = M extends ReducersMapObject< */ export function combineReducers( reducers: ReducersMapObject -): Reducer +): Reducer> export function combineReducers( reducers: ReducersMapObject -): Reducer +): Reducer, A> export function combineReducers>( reducers: M -): Reducer, ActionFromReducersMapObject> +): Reducer< + CombinedState>, + ActionFromReducersMapObject +> /* store */ @@ -316,7 +359,7 @@ export interface StoreCreator { ): Store & Ext ( reducer: Reducer, - preloadedState?: DeepPartial, + preloadedState?: PreloadedState, enhancer?: StoreEnhancer ): Store & Ext } @@ -380,7 +423,7 @@ export type StoreEnhancerStoreCreator = < A extends Action = AnyAction >( reducer: Reducer, - preloadedState?: DeepPartial + preloadedState?: PreloadedState ) => Store & Ext /* middleware */ diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 4670f29f78..7259b4884b 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -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' @@ -43,10 +37,10 @@ function stateExtension() { A extends Action = AnyAction >( reducer: Reducer, - preloadedState?: DeepPartial + preloadedState?: PreloadedState ) => { const wrappedReducer: Reducer = null as any - const wrappedPreloadedState: S & ExtraState = null as any + const wrappedPreloadedState: PreloadedState = null as any return createStore(wrappedReducer, wrappedPreloadedState) } diff --git a/test/typescript/store.ts b/test/typescript/store.ts index fb1974ab41..123464ce55 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -57,21 +57,45 @@ const funcWithStore = (store: Store) => {} const store: Store = createStore(reducer) const storeWithPreloadedState: Store = createStore(reducer, { + a: 'a', + b: { c: 'c', d: 'd' } +}) +// typings:expect-error +const storeWithBadPreloadedState: Store = 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 = createStore(reducer, enhancer) const storeWithPreloadedStateAndEnhancer: Store = createStore( + reducer, + { + a: 'a', + b: { c: 'c', d: 'd' } + }, + enhancer +) + +// typings:expect-error +const storeWithBadPreloadedStateAndEnhancer: Store = createStore( reducer, { b: { c: 'c' }