From 0ac73b52f17e7b91487cd5fa0b80bcdad1b344dc Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Mon, 12 Aug 2019 08:24:26 -0700 Subject: [PATCH] #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 --- docs/recipes/UsingObjectSpreadOperator.md | 3 +- index.d.ts | 53 ++++++++++++++++++++--- test/typescript/enhancers.ts | 14 ++---- test/typescript/store.ts | 26 ++++++++++- 4 files changed, 79 insertions(+), 17 deletions(-) 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' }