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
53 changes: 47 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S> = { readonly [$CombinedState]: undefined } & S

/**
* Helper to extract the raw state from a `CombinedState` type.
*/
export type UnCombinedState<S> = S extends CombinedState<infer S1> ? 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> = S extends CombinedState<infer S1>
? {
[K in keyof S1]?: S[K] extends object ? PreloadedState<S[K]> : S[K]
}
: {
[K in keyof S]: S[K] extends object ? PreloadedState<S[K]> : S[K]
}

/* reducers */

/**
Expand Down Expand Up @@ -59,9 +100,9 @@ export interface AnyAction extends Action {
* @template A The type of actions the reducer can potentially respond to.
*/
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
state: UnCombinedState<S> | undefined,
action: A
) => S
) => UnCombinedState<S>

/**
* Object whose values correspond to different reducer functions.
Expand Down Expand Up @@ -92,10 +133,10 @@ export type ReducersMapObject<S = any, A extends Action = Action> = {
*/
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>

/* store */

Expand Down Expand Up @@ -269,7 +310,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 @@ -333,7 +374,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
5 changes: 3 additions & 2 deletions test/typescript/enhancers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PreloadedState } from '../../index'
import {
StoreEnhancer,
Action,
Expand Down Expand Up @@ -43,10 +44,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