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
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>
timdorr marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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