From a02a58ffbd429a67cc3fd2d49a86a2d287e9e9c4 Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Mon, 29 Jul 2019 19:46:47 -0700 Subject: [PATCH 1/6] Preloaded state is now selectively partial (instead of deeply partial). --- index.d.ts | 53 ++++++++++++++++++++++++++++++++---- test/typescript/enhancers.ts | 5 ++-- test/typescript/store.ts | 26 +++++++++++++++++- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index c1a16080bf..73cf6e7c66 100644 --- a/index.d.ts +++ b/index.d.ts @@ -32,6 +32,47 @@ 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 "sticky" when we cast to it. + */ +export type CombinedState = { readonly [$CombinedState]: undefined } & S + +/** + * Helper to extract the raw state from a `CombinedState` type. + */ +export type UnCombinedState = S extends CombinedState ? S1 : 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 extends CombinedState + ? { + [K in keyof S1]?: S[K] extends object ? PreloadedState : S[K] + } + : { + [K in keyof S]: S[K] extends object ? PreloadedState : S[K] + } + /* reducers */ /** @@ -59,9 +100,9 @@ export interface AnyAction extends Action { * @template A The type of actions the reducer can potentially respond to. */ export type Reducer = ( - state: S | undefined, + state: UnCombinedState | undefined, action: A -) => S +) => UnCombinedState /** * Object whose values correspond to different reducer functions. @@ -92,10 +133,10 @@ export type ReducersMapObject = { */ export function combineReducers( reducers: ReducersMapObject -): Reducer +): Reducer> export function combineReducers( reducers: ReducersMapObject -): Reducer +): Reducer, A> /* store */ @@ -269,7 +310,7 @@ export interface StoreCreator { ): Store & Ext ( reducer: Reducer, - preloadedState?: DeepPartial, + preloadedState?: PreloadedState, enhancer?: StoreEnhancer ): Store & Ext } @@ -333,7 +374,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..e9d48fee95 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -1,3 +1,4 @@ +import { PreloadedState } from '../../index' import { StoreEnhancer, Action, @@ -43,10 +44,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' } From 0259308ffcd770accc59e66f69b4f5cb2239817f Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Mon, 29 Jul 2019 23:53:49 -0700 Subject: [PATCH 2/6] 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. --- index.d.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index 73cf6e7c66..78fa85a8fc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -53,22 +53,21 @@ declare const $CombinedState: unique symbol * typed as always undefined, so its never expected to have a meaningful * value anyway. It just makes this type "sticky" when we cast to it. */ -export type CombinedState = { readonly [$CombinedState]: undefined } & S - -/** - * Helper to extract the raw state from a `CombinedState` type. - */ -export type UnCombinedState = S extends CombinedState ? S1 : S +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 = S extends CombinedState - ? { - [K in keyof S1]?: S[K] extends object ? PreloadedState : S[K] - } +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] } @@ -100,9 +99,9 @@ export type PreloadedState = S extends CombinedState * @template A The type of actions the reducer can potentially respond to. */ export type Reducer = ( - state: UnCombinedState | undefined, + state: S | undefined, action: A -) => UnCombinedState +) => S /** * Object whose values correspond to different reducer functions. From 5d3871357e929ebd61da508fb685a7270c1b5736 Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Tue, 30 Jul 2019 00:07:51 -0700 Subject: [PATCH 3/6] Small change to the description of CombinedState. --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 78fa85a8fc..1787fd8422 100644 --- a/index.d.ts +++ b/index.d.ts @@ -51,7 +51,7 @@ declare const $CombinedState: unique symbol * (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 "sticky" when we cast to it. + * value anyway. It just makes this type distinquishable from plain `{}`. */ export type CombinedState = { readonly [$CombinedState]?: undefined } & S From 0020860be444b4b62564ee6b7d9f435da49f6d1e Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Fri, 2 Aug 2019 08:46:22 -0700 Subject: [PATCH 4/6] Removed DeepPartial import from tests. Leaving the definition in place as removing it would be a breaking change. --- test/typescript/enhancers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index e9d48fee95..1ccf707eb3 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -4,8 +4,7 @@ import { Action, AnyAction, Reducer, - createStore, - DeepPartial + createStore } from 'redux' interface State { From 1ec118848ca77b9b5694ab703270675fd8347d1c Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Fri, 2 Aug 2019 08:52:25 -0700 Subject: [PATCH 5/6] Made prettier happy. --- test/typescript/enhancers.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 1ccf707eb3..7259b4884b 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -1,11 +1,5 @@ import { PreloadedState } from '../../index' -import { - StoreEnhancer, - Action, - AnyAction, - Reducer, - createStore -} from 'redux' +import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from 'redux' interface State { someField: 'string' From 22b770b53345e7c9f1d5d5d340b8edad217b1b67 Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Fri, 2 Aug 2019 09:26:38 -0700 Subject: [PATCH 6/6] Made prettier happy with UsingObjectSpreadOperator.md --- docs/recipes/UsingObjectSpreadOperator.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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.