Repository: pmndrs/zustand
Pull request #941 (2022-04-29 02:11:56)

fix(types): avoid any in react.ts
There was a `TODO` in react.ts.
I originally thought this requires StoreMutator, but it doesn't actually mutate. So, it's rather simple?

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/pmndrs/zustand/pr/941/builds/244501) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 99d505459929934bbb074dc539062bbda0e1d69f:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-wwqvms)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-5drbdb)| Configuration |
|[React Browserify](https://codesandbox.io/s/react-browserify-i31hc4)| Configuration |
|[React Snowpack](https://codesandbox.io/s/react-snowpack-6wu1l1)| Configuration |
|[React Parcel](https://codesandbox.io/s/react-parcel-7vvw4n)| Configuration |
|[Next.js](https://codesandbox.io/s/next-js-e89h4z)| Configuration |

github-actions[bot]: **Size Change:** +1 B (0%) 

**Total Size:** 29 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `dist/system/index.production.js` | 386 B | +1 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `dist/context.js` | 518 B |
| `dist/esm/context.js` | 448 B |
| `dist/esm/index.js` | 400 B |
| `dist/esm/middleware.js` | 2.95 kB |
| `dist/esm/middleware/immer.js` | 210 B |
| `dist/esm/shallow.js` | 272 B |
| `dist/esm/vanilla.js` | 377 B |
| `dist/index.js` | 642 B |
| `dist/middleware.js` | 3.07 kB |
| `dist/middleware/immer.js` | 373 B |
| `dist/shallow.js` | 318 B |
| `dist/system/context.development.js` | 572 B |
| `dist/system/context.production.js` | 393 B |
| `dist/system/index.development.js` | 575 B |
| `dist/system/middleware.development.js` | 3.04 kB |
| `dist/system/middleware.production.js` | 1.97 kB |
| `dist/system/middleware/immer.development.js` | 292 B |
| `dist/system/middleware/immer.production.js` | 187 B |
| `dist/system/shallow.development.js` | 338 B |
| `dist/system/shallow.production.js` | 253 B |
| `dist/system/vanilla.development.js` | 444 B |
| `dist/system/vanilla.production.js` | 270 B |
| `dist/umd/context.development.js` | 662 B |
| `dist/umd/context.production.js` | 488 B |
| `dist/umd/index.development.js` | 787 B |
| `dist/umd/index.production.js` | 564 B |
| `dist/umd/middleware.development.js` | 3.18 kB |
| `dist/umd/middleware.production.js` | 1.97 kB |
| `dist/umd/middleware/immer.development.js` | 514 B |
| `dist/umd/middleware/immer.production.js` | 371 B |
| `dist/umd/shallow.development.js` | 447 B |
| `dist/umd/shallow.production.js` | 344 B |
| `dist/umd/vanilla.development.js` | 590 B |
| `dist/umd/vanilla.production.js` | 378 B |
| `dist/vanilla.js` | 456 B |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>

Patch:
diff --git a/docs/typescript.md b/docs/typescript.md
index cfa6cc2..9a06978 100644
--- a/docs/typescript.md
+++ b/docs/typescript.md
@@ -21,6 +21,8 @@ const useStore = create<BearState>()((set) => ({
 <details>
   <summary>Why can't we just simply infer the type from initial state?</summary>
 
+  <br/>
+
   **TLDR**: Because state generic `T` is invariant.
   
   Consider this minimal version `create`...
@@ -69,6 +71,8 @@ const useStore = create<BearState>()((set) => ({
 
 <details>
   <summary>Why that currying `()(...)`?</summary>
+
+  <br/>
   
   **TLDR**: It's a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571).
 
@@ -118,6 +122,8 @@ const useStore = create(combine({ bears: 0 }, (set) => ({
 <details>
   <summary>But be a little careful...</summary>
 
+  <br/>
+
   We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the first parameter only when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. So for example `get` from the second parameter has type `() => { bears: number }` and that's a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useStore` still has the correct type, ie for example `useStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`.
 
   It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so in most cases there won't be a problem. Just you have to be careful while using replace. For eg `set({ bears: 0 }, true)` would compile but will be unsound as it'll delete the `increase` function. (If you set from "outside" ie `useStore.setState({ bears: 0 }, true)` then it won't compile because the "outside" store knows that `increase` is missing.) Another instance where you should be careful you're doing `Object.keys`, `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this).
diff --git a/readme.md b/readme.md
index 10722c9..a52d8ee 100644
--- a/readme.md
+++ b/readme.md
@@ -538,6 +538,8 @@ A more complete TypeScript guide is [here](https://github.com/pmndrs/zustand/blo
 <details>
 <summary>Calling actions outside a React event handler in pre React 18</summary>
 
+<br/>
+
 Because React handles `setState` synchronously if it's called outside an event handler. Updating the state outside an event handler will force react to update the components synchronously, therefore adding the risk of encountering the zombie-child effect.
 In order to fix this, the action needs to be wrapped in `unstable_batchedUpdates`
 
diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts
index 144458a..33c69a4 100644
--- a/src/middleware/devtools.ts
+++ b/src/middleware/devtools.ts
@@ -44,9 +44,9 @@ type TakeTwo<T> = T extends []
   ? [A0?, A1?]
   : never
 
-type WithDevtools<S> = Write<Cast<S, object>, StoreSetStateWithAction<S>>
+type WithDevtools<S> = Write<Cast<S, object>, StoreDevtools<S>>
 
-type StoreSetStateWithAction<S> = S extends {
+type StoreDevtools<S> = S extends {
   setState: (...a: infer A) => infer Sr
 }
   ? {
@@ -58,8 +58,8 @@ type StoreSetStateWithAction<S> = S extends {
 
 interface DevtoolsOptions {
   enabled?: boolean
-  name?: string
   anonymousActionType?: string
+  name?: string
   serialize?:
     | boolean
     | {
@@ -81,7 +81,7 @@ type Devtools = <
   Mcs extends [StoreMutatorIdentifier, unknown][] = []
 >(
   initializer: StateCreator<T, [...Mps, ['zustand/devtools', never]], Mcs>,
-  options?: DevtoolsOptions
+  devtoolsOptions?: DevtoolsOptions
 ) => StateCreator<T, Mps, [['zustand/devtools', never], ...Mcs]>
 
 declare module '../vanilla' {
@@ -93,7 +93,7 @@ declare module '../vanilla' {
 
 type DevtoolsImpl = <T extends State>(
   storeInitializer: PopArgument<StateCreator<T, [], []>>,
-  options: DevtoolsOptions
+  devtoolsOptions?: DevtoolsOptions
 ) => PopArgument<StateCreator<T, [], []>>
 
 type PopArgument<T extends (...a: never[]) => unknown> = T extends (
@@ -104,155 +104,152 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
 
 export type NamedSet<T extends State> = WithDevtools<StoreApi<T>>['setState']
 
-const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => {
-  type S = ReturnType<typeof fn>
-
-  const devtoolsOptions =
-    options === undefined
-      ? {}
-      : typeof options === 'string'
-      ? { name: options }
-      : options
-
-  const { enabled } = devtoolsOptions as { enabled?: boolean }
-  let extensionConnector: typeof window['__REDUX_DEVTOOLS_EXTENSION__'] | false
-  try {
-    extensionConnector =
-      (enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__
-  } catch {
-    // ignored
-  }
+const devtoolsImpl: DevtoolsImpl =
+  (fn, devtoolsOptions = {}) =>
+  (set, get, api) => {
+    type S = ReturnType<typeof fn>
 
-  if (!extensionConnector) {
-    if (__DEV__ && enabled) {
-      console.warn(
-        '[zustand devtools middleware] Please install/enable Redux devtools extension'
-      )
+    const { enabled, anonymousActionType, ...options } = devtoolsOptions
+    let extensionConnector:
+      | typeof window['__REDUX_DEVTOOLS_EXTENSION__']
+      | false
+    try {
+      extensionConnector =
+        (enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__
+    } catch {
+      // ignored
     }
-    return fn(set, get, api)
-  }
-
-  const extension = extensionConnector.connect(devtoolsOptions)
-
-  let isRecording = true
-  ;(api.setState as NamedSet<S>) = (state, replace, nameOrAction) => {
-    const r = set(state, replace)
-    if (!isRecording) return r
-    extension.send(
-      nameOrAction === undefined
-        ? { type: devtoolsOptions.anonymousActionType || 'anonymous' }
-        : typeof nameOrAction === 'string'
-        ? { type: nameOrAction }
-        : nameOrAction,
-      get()
-    )
-    return r
-  }
-  const setStateFromDevtools: SetState<S> = (...a) => {
-    const originalIsRecording = isRecording
-    isRecording = false
-    set(...a)
-    isRecording = originalIsRecording
-  }
-
-  const initialState = fn(api.setState, get, api)
-  extension.init(initialState)
 
-  if (
-    (api as any).dispatchFromDevtools &&
-    typeof (api as any).dispatch === 'function'
-  ) {
-    let didWarnAboutReservedActionType = false
-    const originalDispatch = (api as any).dispatch
-    ;(api as any).dispatch = (...a: any[]) => {
-      if (
-        __DEV__ &&
-        a[0].type === '__setState' &&
-        !didWarnAboutReservedActionType
-      ) {
+    if (!extensionConnector) {
+      if (__DEV__ && enabled) {
         console.warn(
-          '[zustand devtools middleware] "__setState" action type is reserved ' +
-            'to set state from the devtools. Avoid using it.'
+          '[zustand devtools middleware] Please install/enable Redux devtools extension'
         )
-        didWarnAboutReservedActionType = true
       }
-      ;(originalDispatch as any)(...a)
+      return fn(set, get, api)
     }
-  }
 
-  ;(
-    extension as unknown as {
-      // FIXME https://github.com/reduxjs/redux-devtools/issues/1097
-      subscribe: (
-        listener: (message: Message) => void
-      ) => (() => void) | undefined
+    const extension = extensionConnector.connect(options)
+
+    let isRecording = true
+    ;(api.setState as NamedSet<S>) = (state, replace, nameOrAction) => {
+      const r = set(state, replace)
+      if (!isRecording) return r
+      extension.send(
+        nameOrAction === undefined
+          ? { type: anonymousActionType || 'anonymous' }
+          : typeof nameOrAction === 'string'
+          ? { type: nameOrAction }
+          : nameOrAction,
+        get()
+      )
+      return r
     }
-  ).subscribe((message: any) => {
-    switch (message.type) {
-      case 'ACTION':
-        if (typeof message.payload !== 'string') {
-          console.error(
-            '[zustand devtools middleware] Unsupported action format'
+    const setStateFromDevtools: SetState<S> = (...a) => {
+      const originalIsRecording = isRecording
+      isRecording = false
+      set(...a)
+      isRecording = originalIsRecording
+    }
+
+    const initialState = fn(api.setState, get, api)
+    extension.init(initialState)
+
+    if (
+      (api as any).dispatchFromDevtools &&
+      typeof (api as any).dispatch === 'function'
+    ) {
+      let didWarnAboutReservedActionType = false
+      const originalDispatch = (api as any).dispatch
+      ;(api as any).dispatch = (...a: any[]) => {
+        if (
+          __DEV__ &&
+          a[0].type === '__setState' &&
+          !didWarnAboutReservedActionType
+        ) {
+          console.warn(
+            '[zustand devtools middleware] "__setState" action type is reserved ' +
+              'to set state from the devtools. Avoid using it.'
           )
-          return
+          didWarnAboutReservedActionType = true
         }
-        return parseJsonThen<{ type: unknown; state?: PartialState<S> }>(
-          message.payload,
-          (action) => {
-            if (action.type === '__setState') {
-              setStateFromDevtools(action.state as PartialState<S>)
-              return
-            }
+        ;(originalDispatch as any)(...a)
+      }
+    }
 
-            if (!(api as any).dispatchFromDevtools) return
-            if (typeof (api as any).dispatch !== 'function') return
-            ;(api as any).dispatch(action)
+    ;(
+      extension as unknown as {
+        // FIXME https://github.com/reduxjs/redux-devtools/issues/1097
+        subscribe: (
+          listener: (message: Message) => void
+        ) => (() => void) | undefined
+      }
+    ).subscribe((message: any) => {
+      switch (message.type) {
+        case 'ACTION':
+          if (typeof message.payload !== 'string') {
+            console.error(
+              '[zustand devtools middleware] Unsupported action format'
+            )
+            return
           }
-        )
+          return parseJsonThen<{ type: unknown; state?: PartialState<S> }>(
+            message.payload,
+            (action) => {
+              if (action.type === '__setState') {
+                setStateFromDevtools(action.state as PartialState<S>)
+                return
+              }
 
-      case 'DISPATCH':
-        switch (message.payload.type) {
-          case 'RESET':
-            setStateFromDevtools(initialState)
-            return extension.init(api.getState())
+              if (!(api as any).dispatchFromDevtools) return
+              if (typeof (api as any).dispatch !== 'function') return
+              ;(api as any).dispatch(action)
+            }
+          )
 
-          case 'COMMIT':
-            return extension.init(api.getState())
+        case 'DISPATCH':
+          switch (message.payload.type) {
+            case 'RESET':
+              setStateFromDevtools(initialState)
+              return extension.init(api.getState())
 
-          case 'ROLLBACK':
-            return parseJsonThen<S>(message.state, (state) => {
-              setStateFromDevtools(state)
-              extension.init(api.getState())
-            })
+            case 'COMMIT':
+              return extension.init(api.getState())
 
-          case 'JUMP_TO_STATE':
-          case 'JUMP_TO_ACTION':
-            return parseJsonThen<S>(message.state, (state) => {
-              setStateFromDevtools(state)
-            })
+            case 'ROLLBACK':
+              return parseJsonThen<S>(message.state, (state) => {
+                setStateFromDevtools(state)
+                extension.init(api.getState())
+              })
 
-          case 'IMPORT_STATE': {
-            const { nextLiftedState } = message.payload
-            const lastComputedState =
-              nextLiftedState.computedStates.slice(-1)[0]?.state
-            if (!lastComputedState) return
-            setStateFromDevtools(lastComputedState)
-            extension.send(
-              null as any, // FIXME no-any
-              nextLiftedState
-            )
-            return
-          }
+            case 'JUMP_TO_STATE':
+            case 'JUMP_TO_ACTION':
+              return parseJsonThen<S>(message.state, (state) => {
+                setStateFromDevtools(state)
+              })
 
-          case 'PAUSE_RECORDING':
-            return (isRecording = !isRecording)
-        }
-        return
-    }
-  })
+            case 'IMPORT_STATE': {
+              const { nextLiftedState } = message.payload
+              const lastComputedState =
+                nextLiftedState.computedStates.slice(-1)[0]?.state
+              if (!lastComputedState) return
+              setStateFromDevtools(lastComputedState)
+              extension.send(
+                null as any, // FIXME no-any
+                nextLiftedState
+              )
+              return
+            }
 
-  return initialState
-}
+            case 'PAUSE_RECORDING':
+              return (isRecording = !isRecording)
+          }
+          return
+      }
+    })
+
+    return initialState
+  }
 export const devtools = devtoolsImpl as unknown as Devtools
 
 const parseJsonThen = <T>(stringified: string, f: (parsed: T) => void) => {
diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts
index 933b004..10663be 100644
--- a/src/middleware/immer.ts
+++ b/src/middleware/immer.ts
@@ -18,6 +18,7 @@ declare module '../vanilla' {
 }
 
 type Write<T extends object, U extends object> = Omit<T, keyof U> & U
+type Cast<T, U> = T extends U ? T : U
 type SkipTwo<T> = T extends []
   ? []
   : T extends [unknown]
@@ -32,22 +33,21 @@ type SkipTwo<T> = T extends []
   ? A
   : never
 
-type WithImmer<S> = S extends {
+type WithImmer<S> = Write<Cast<S, object>, StoreImmer<S>>
+
+type StoreImmer<S> = S extends {
   getState: () => infer T
   setState: infer SetState
 }
-  ? Write<
-      S,
-      SetState extends (...a: infer A) => infer Sr
-        ? {
-            setState(
-              nextStateOrUpdater: T | Partial<T> | ((state: Draft<T>) => void),
-              shouldReplace?: boolean | undefined,
-              ...a: SkipTwo<A>
-            ): Sr
-          }
-        : never
-    >
+  ? SetState extends (...a: infer A) => infer Sr
+    ? {
+        setState(
+          nextStateOrUpdater: T | Partial<T> | ((state: Draft<T>) => void),
+          shouldReplace?: boolean | undefined,
+          ...a: SkipTwo<A>
+        ): Sr
+      }
+    : never
   : never
 
 type PopArgument<T extends (...a: never[]) => unknown> = T extends (
diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts
index 8bffda9..aece77e 100644
--- a/src/middleware/redux.ts
+++ b/src/middleware/redux.ts
@@ -4,15 +4,15 @@ import { NamedSet } from './devtools'
 type Write<T extends object, U extends object> = Omit<T, keyof U> & U
 type Cast<T, U> = T extends U ? T : U
 
-interface Action {
+type Action = {
   type: unknown
 }
 
-interface ReduxState<A extends Action> {
+type ReduxState<A extends Action> = {
   dispatch: StoreRedux<A>['dispatch']
 }
 
-interface StoreRedux<A extends Action> {
+type StoreRedux<A extends Action> = {
   dispatch: (a: A) => A
   dispatchFromDevtools: true
 }
diff --git a/src/middleware/subscribeWithSelector.ts b/src/middleware/subscribeWithSelector.ts
index 70ba705..a91e527 100644
--- a/src/middleware/subscribeWithSelector.ts
+++ b/src/middleware/subscribeWithSelector.ts
@@ -32,7 +32,7 @@ declare module '../vanilla' {
   }
 }
 
-interface StoreSubscribeWithSelector<T extends State> {
+type StoreSubscribeWithSelector<T extends State> = {
   subscribe: {
     (listener: (selectedState: T, previousSelectedState: T) => void): () => void
     <U>(
diff --git a/src/react.ts b/src/react.ts
index e5af6c7..504b82a 100644
--- a/src/react.ts
+++ b/src/react.ts
@@ -10,22 +10,31 @@ import createStore, {
   StoreMutatorIdentifier,
 } from './vanilla'
 
-export function useStore<S extends StoreApi<State>>(api: S): ExtractState<S>
-export function useStore<S extends StoreApi<State>, U>(
+type ExtractState<S> = S extends { getState: () => infer T } ? T : never
+
+type WithReact<S extends StoreApi<State>> = S & {
+  getServerState?: () => ExtractState<S>
+}
+
+export function useStore<S extends WithReact<StoreApi<State>>>(
+  api: S
+): ExtractState<S>
+
+export function useStore<S extends WithReact<StoreApi<State>>, U>(
   api: S,
   selector: StateSelector<ExtractState<S>, U>,
   equalityFn?: EqualityChecker<U>
 ): U
+
 export function useStore<TState extends State, StateSlice>(
-  api: StoreApi<TState>,
+  api: WithReact<StoreApi<TState>>,
   selector: StateSelector<TState, StateSlice> = api.getState as any,
   equalityFn?: EqualityChecker<StateSlice>
 ) {
   const slice = useSyncExternalStoreWithSelector(
     api.subscribe,
     api.getState,
-    // TODO avoid `any` and add type only in react.ts
-    (api as any).getServerState || api.getState,
+    api.getServerState || api.getState,
     selector,
     equalityFn
   )
@@ -33,9 +42,7 @@ export function useStore<TState extends State, StateSlice>(
   return slice
 }
 
-type ExtractState<S> = S extends { getState: () => infer T } ? T : never
-
-export type UseBoundStore<S extends StoreApi<State>> = {
+export type UseBoundStore<S extends WithReact<StoreApi<State>>> = {
   (): ExtractState<S>
   <U>(
     selector: StateSelector<ExtractState<S>, U>,


Test patch:
diff --git a/tests/devtools.test.tsx b/tests/devtools.test.tsx
index f5ffb5b..1b39612 100644
--- a/tests/devtools.test.tsx
+++ b/tests/devtools.test.tsx
@@ -26,9 +26,9 @@ beforeEach(() => {
 })
 
 it('connects to the extension by passing the options and initializes', () => {
-  const options = { name: 'test', foo: 'bar', enabled: true }
+  const options = { name: 'test', foo: 'bar' }
   const initialState = { count: 0 }
-  create(devtools(() => initialState, options))
+  create(devtools(() => initialState, { enabled: true, ...options }))
 
   expect(extensionConnector.connect).toHaveBeenLastCalledWith(options)
   expect(extension.init).toHaveBeenLastCalledWith(initialState)