Repository: pmndrs/zustand
Pull request #1144 (2022-07-27 11:52:33)

feat(vanilla): non-object state
and simplify types (eliminating `Cast`).

---

There might not be many use cases, but maybe we can support non-object state.

Counter example: https://codesandbox.io/s/react-typescript-forked-uvyys3?file=/src/App.tsx

```tsx
import create from "zustand";

const useCount = create(() => 0);
const inc = () => useCount.setState((c) => c + 1);

const Counter = () => {
  const count = useCount();
  return (
    <div>
      {count} <button onClick={inc}>+1</button>
    </div>
  );
};
```


### TODOs

- [x] vanilla types&impl
- [x] react types
- [x] middleware
  - combine: object only
  - [x] devtools: theoretically accepts non-object
  - [x] immer: theoretically accepts non-object
  - [x] persist: supports non-object
  - [x] redux: theoretically accepts non-object
  - [x] subscribeWithSelector: supports non-object
- [x] tests/basic.test.tsx

codesandbox-ci[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/1144/builds/275477) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e14f98d5e81337701fc1aa564424e279da1c5719:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-6b42kq)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-v6l9zb)| Configuration |
|[React Browserify](https://codesandbox.io/s/react-browserify-uc1oot)| Configuration |
|[React Snowpack](https://codesandbox.io/s/react-snowpack-5ssg6l)| Configuration |
|[React Parcel](https://codesandbox.io/s/react-parcel-md4kr8)| Configuration |
|[Next.js](https://codesandbox.io/s/next-js-xf6inr)| Configuration |

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

**Total Size:** 29.1 kB

| Filename | Size | Change |  |
| :--- | :---: | :---: | :---: |
| `dist/esm/vanilla.js` | 402 B | +17 B (+4%) |  |
| `dist/system/vanilla.development.js` | 471 B | +20 B (+4%) |  |
| `dist/system/vanilla.production.js` | 285 B | +15 B (+6%) | 🔍 |
| `dist/umd/vanilla.development.js` | 607 B | +17 B (+3%) |  |
| `dist/umd/vanilla.production.js` | 390 B | +12 B (+3%) |  |
| `dist/vanilla.js` | 475 B | +19 B (+4%) |  |

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

| Filename | Size |
| :--- | :---: |
| `dist/context.js` | 518 B |
| `dist/esm/context.js` | 464 B |
| `dist/esm/index.js` | 426 B |
| `dist/esm/middleware.js` | 2.73 kB |
| `dist/esm/middleware/immer.js` | 210 B |
| `dist/esm/shallow.js` | 272 B |
| `dist/index.js` | 670 B |
| `dist/middleware.js` | 3.24 kB |
| `dist/middleware/immer.js` | 373 B |
| `dist/shallow.js` | 318 B |
| `dist/system/context.development.js` | 589 B |
| `dist/system/context.production.js` | 393 B |
| `dist/system/index.development.js` | 602 B |
| `dist/system/index.production.js` | 393 B |
| `dist/system/middleware.development.js` | 2.82 kB |
| `dist/system/middleware.production.js` | 1.75 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/umd/context.development.js` | 662 B |
| `dist/umd/context.production.js` | 488 B |
| `dist/umd/index.development.js` | 816 B |
| `dist/umd/index.production.js` | 570 B |
| `dist/umd/middleware.development.js` | 3.35 kB |
| `dist/umd/middleware.production.js` | 2.06 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 |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
dai-shi: @devanshj Can you help typing middlewares? This is not in hurry.
devanshj: > @devanshj Can you help typing middlewares? This is not in hurry.

I think things should change much, this would mainly involve removing `extends State` from places. Give it a try and then if you're finding it difficult then let me know and I'll help.
dai-shi: Yeah, I already tried and lost confident, especially around `Cast`.
I can try that again and ask for review if that's easier.
devanshj: > Yeah, I already tried and lost confident, especially around Cast.

Ah okay then I'll look into it hopefully soon.

> I can try that again and ask for review if that's easier.

Yeah if you can tell me specific places where you got stuck then I can solve it and explain it to you and it will also make you understand the types better.

Patch:
diff --git a/src/context.ts b/src/context.ts
index 135cbf4..6837ae0 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -8,7 +8,7 @@ import {
 } from 'react'
 import { StoreApi, useStore } from 'zustand'
 
-type UseContextStore<S extends StoreApi> = {
+type UseContextStore<S extends StoreApi<unknown>> = {
   (): ExtractState<S>
   <U>(
     selector: (state: ExtractState<S>) => U,
@@ -20,7 +20,7 @@ type ExtractState<S> = S extends { getState: () => infer T } ? T : never
 
 type WithoutCallSignature<T> = { [K in keyof T]: T[K] }
 
-function createContext<S extends StoreApi>() {
+function createContext<S extends StoreApi<unknown>>() {
   const ZustandContext = reactCreateContext<S | undefined>(undefined)
 
   const Provider = ({
diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts
index af72f07..0602a7c 100644
--- a/src/middleware/devtools.ts
+++ b/src/middleware/devtools.ts
@@ -15,8 +15,7 @@ type Message = {
   state?: any
 }
 
-type Write<T extends object, U extends object> = Omit<T, keyof U> & U
-type Cast<T, U> = T extends U ? T : U
+type Write<T, U> = Omit<T, keyof U> & U
 type TakeTwo<T> = T extends []
   ? [undefined, undefined]
   : T extends [unknown]
@@ -37,7 +36,7 @@ type TakeTwo<T> = T extends []
   ? [A0?, A1?]
   : never
 
-type WithDevtools<S> = Write<Cast<S, object>, StoreDevtools<S>>
+type WithDevtools<S> = Write<S, StoreDevtools<S>>
 
 type StoreDevtools<S> = S extends {
   setState: (...a: infer Sa) => infer Sr
@@ -49,6 +48,9 @@ type StoreDevtools<S> = S extends {
     }
   : never
 
+const isObjectWithTypeProperty = (x: unknown): x is { type: unknown } =>
+  x !== null && typeof x === 'object' && 'type' in x
+
 export interface DevtoolsOptions {
   enabled?: boolean
   anonymousActionType?: string
@@ -69,7 +71,7 @@ export interface DevtoolsOptions {
 }
 
 type Devtools = <
-  T extends object,
+  T,
   Mps extends [StoreMutatorIdentifier, unknown][] = [],
   Mcs extends [StoreMutatorIdentifier, unknown][] = []
 >(
@@ -84,7 +86,7 @@ declare module '../vanilla' {
   }
 }
 
-type DevtoolsImpl = <T extends object>(
+type DevtoolsImpl = <T>(
   storeInitializer: PopArgument<StateCreator<T, [], []>>,
   devtoolsOptions?: DevtoolsOptions
 ) => PopArgument<StateCreator<T, [], []>>
@@ -95,7 +97,7 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
   ? (...a: A) => R
   : never
 
-export type NamedSet<T extends object> = WithDevtools<StoreApi<T>>['setState']
+export type NamedSet<T> = WithDevtools<StoreApi<T>>['setState']
 
 const devtoolsImpl: DevtoolsImpl =
   (fn, devtoolsOptions = {}) =>
@@ -132,9 +134,9 @@ const devtoolsImpl: DevtoolsImpl =
       extension.send(
         nameOrAction === undefined
           ? { type: anonymousActionType || 'anonymous' }
-          : typeof nameOrAction === 'string'
-          ? { type: nameOrAction }
-          : nameOrAction,
+          : isObjectWithTypeProperty(nameOrAction)
+          ? nameOrAction
+          : { type: nameOrAction },
         get()
       )
       return r
diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts
index 2f453b1..76e2ec7 100644
--- a/src/middleware/immer.ts
+++ b/src/middleware/immer.ts
@@ -3,7 +3,7 @@ import { Draft, produce } from 'immer'
 import { StateCreator, StoreMutatorIdentifier } from '../vanilla'
 
 type Immer = <
-  T extends object,
+  T,
   Mps extends [StoreMutatorIdentifier, unknown][] = [],
   Mcs extends [StoreMutatorIdentifier, unknown][] = []
 >(
@@ -17,8 +17,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 Write<T, U> = Omit<T, keyof U> & U
 type SkipTwo<T> = T extends []
   ? []
   : T extends [unknown]
@@ -33,7 +32,7 @@ type SkipTwo<T> = T extends []
   ? A
   : never
 
-type WithImmer<S> = Write<Cast<S, object>, StoreImmer<S>>
+type WithImmer<S> = Write<S, StoreImmer<S>>
 
 type StoreImmer<S> = S extends {
   getState: () => infer T
@@ -56,7 +55,7 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
   ? (...a: A) => R
   : never
 
-type ImmerImpl = <T extends object>(
+type ImmerImpl = <T>(
   storeInitializer: PopArgument<StateCreator<T, [], []>>
 ) => PopArgument<StateCreator<T, [], []>>
 
diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts
index 2a9b009..d7fc263 100644
--- a/src/middleware/persist.ts
+++ b/src/middleware/persist.ts
@@ -72,7 +72,7 @@ export interface PersistOptions<S, PersistedState = S> {
 
 type PersistListener<S> = (state: S) => void
 
-type StorePersist<S extends object, Ps> = {
+type StorePersist<S, Ps> = {
   persist: {
     setOptions: (options: Partial<PersistOptions<S, Ps>>) => void
     clearStorage: () => void
@@ -297,7 +297,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
 }
 
 type Persist = <
-  T extends object,
+  T,
   Mps extends [StoreMutatorIdentifier, unknown][] = [],
   Mcs extends [StoreMutatorIdentifier, unknown][] = [],
   U = T
@@ -312,14 +312,13 @@ 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 Write<T, U> = Omit<T, keyof U> & U
 
 type WithPersist<S, A> = S extends { getState: () => infer T }
-  ? Write<S, StorePersist<Cast<T, object>, A>>
+  ? Write<S, StorePersist<T, A>>
   : never
 
-type PersistImpl = <T extends object>(
+type PersistImpl = <T>(
   storeInitializer: PopArgument<StateCreator<T, [], []>>,
   options: PersistOptions<T, T>
 ) => PopArgument<StateCreator<T, [], []>>
diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts
index 2cc5ef6..264a8f3 100644
--- a/src/middleware/redux.ts
+++ b/src/middleware/redux.ts
@@ -1,29 +1,20 @@
 import { StateCreator, StoreMutatorIdentifier } from '../vanilla'
 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
+type Write<T, U> = Omit<T, keyof U> & U
 
-type Action = {
-  type: unknown
-}
-
-type ReduxState<A extends Action> = {
+type ReduxState<A> = {
   dispatch: StoreRedux<A>['dispatch']
 }
 
-type StoreRedux<A extends Action> = {
+type StoreRedux<A> = {
   dispatch: (a: A) => A
   dispatchFromDevtools: true
 }
 
-type WithRedux<S, A> = Write<Cast<S, object>, StoreRedux<Cast<A, Action>>>
+type WithRedux<S, A> = Write<S, StoreRedux<A>>
 
-type Redux = <
-  T extends object,
-  A extends Action,
-  Cms extends [StoreMutatorIdentifier, unknown][] = []
->(
+type Redux = <T, A, Cms extends [StoreMutatorIdentifier, unknown][] = []>(
   reducer: (state: T, action: A) => T,
   initialState: T
 ) => StateCreator<Write<T, ReduxState<A>>, Cms, [['zustand/redux', A]]>
@@ -40,16 +31,23 @@ type PopArgument<T extends (...a: never[]) => unknown> = T extends (
   ? (...a: A) => R
   : never
 
-type ReduxImpl = <T extends object, A extends Action>(
+type ReduxImpl = <T, A>(
   reducer: (state: T, action: A) => T,
   initialState: T
 ) => PopArgument<StateCreator<T & ReduxState<A>, [], []>>
 
+const isObjectWithTypeProperty = (x: unknown): x is { type: unknown } =>
+  x !== null && typeof x === 'object' && 'type' in x
+
 const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => {
   type S = typeof initial
   type A = Parameters<typeof reducer>[1]
   ;(api as any).dispatch = (action: A) => {
-    ;(set as NamedSet<S>)((state: S) => reducer(state, action), false, action)
+    ;(set as NamedSet<S>)(
+      (state: S) => reducer(state, action),
+      false,
+      isObjectWithTypeProperty(action) ? action : { type: action }
+    )
     return action
   }
   ;(api as any).dispatchFromDevtools = true
diff --git a/src/middleware/subscribeWithSelector.ts b/src/middleware/subscribeWithSelector.ts
index 1929633..7a24432 100644
--- a/src/middleware/subscribeWithSelector.ts
+++ b/src/middleware/subscribeWithSelector.ts
@@ -1,7 +1,7 @@
 import { StateCreator, StoreMutatorIdentifier } from '../vanilla'
 
 type SubscribeWithSelector = <
-  T extends object,
+  T,
   Mps extends [StoreMutatorIdentifier, unknown][] = [],
   Mcs extends [StoreMutatorIdentifier, unknown][] = []
 >(
@@ -12,11 +12,10 @@ type SubscribeWithSelector = <
   >
 ) => StateCreator<T, Mps, [['zustand/subscribeWithSelector', never], ...Mcs]>
 
-type Write<T extends object, U extends object> = Omit<T, keyof U> & U
-type Cast<T, U> = T extends U ? T : U
+type Write<T, U> = Omit<T, keyof U> & U
 
 type WithSelectorSubscribe<S> = S extends { getState: () => infer T }
-  ? Write<S, StoreSubscribeWithSelector<Cast<T, object>>>
+  ? Write<S, StoreSubscribeWithSelector<T>>
   : never
 
 declare module '../vanilla' {
@@ -26,7 +25,7 @@ declare module '../vanilla' {
   }
 }
 
-type StoreSubscribeWithSelector<T extends object> = {
+type StoreSubscribeWithSelector<T> = {
   subscribe: {
     (listener: (selectedState: T, previousSelectedState: T) => void): () => void
     <U>(
diff --git a/src/react.ts b/src/react.ts
index 1a47274..5c1671c 100644
--- a/src/react.ts
+++ b/src/react.ts
@@ -15,19 +15,21 @@ const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports
 
 type ExtractState<S> = S extends { getState: () => infer T } ? T : never
 
-type WithReact<S extends StoreApi> = S & {
+type WithReact<S extends StoreApi<unknown>> = S & {
   getServerState?: () => ExtractState<S>
 }
 
-export function useStore<S extends WithReact<StoreApi>>(api: S): ExtractState<S>
+export function useStore<S extends WithReact<StoreApi<unknown>>>(
+  api: S
+): ExtractState<S>
 
-export function useStore<S extends WithReact<StoreApi>, U>(
+export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
   api: S,
   selector: (state: ExtractState<S>) => U,
   equalityFn?: (a: U, b: U) => boolean
 ): U
 
-export function useStore<TState extends object, StateSlice>(
+export function useStore<TState, StateSlice>(
   api: WithReact<StoreApi<TState>>,
   selector: (state: TState) => StateSlice = api.getState as any,
   equalityFn?: (a: StateSlice, b: StateSlice) => boolean
@@ -43,7 +45,7 @@ export function useStore<TState extends object, StateSlice>(
   return slice
 }
 
-export type UseBoundStore<S extends WithReact<StoreApi>> = {
+export type UseBoundStore<S extends WithReact<StoreApi<unknown>>> = {
   (): ExtractState<S>
   <U>(
     selector: (state: ExtractState<S>) => U,
@@ -52,16 +54,16 @@ export type UseBoundStore<S extends WithReact<StoreApi>> = {
 } & S
 
 type Create = {
-  <T extends object, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
+  <T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
     initializer: StateCreator<T, [], Mos>
   ): UseBoundStore<Mutate<StoreApi<T>, Mos>>
-  <T extends object>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
+  <T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
     initializer: StateCreator<T, [], Mos>
   ) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
-  <S extends StoreApi>(store: S): UseBoundStore<S>
+  <S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
 }
 
-const createImpl = <T extends object>(createState: StateCreator<T, [], []>) => {
+const createImpl = <T>(createState: StateCreator<T, [], []>) => {
   const api =
     typeof createState === 'function' ? createStore(createState) : createState
 
@@ -73,8 +75,7 @@ const createImpl = <T extends object>(createState: StateCreator<T, [], []>) => {
   return useBoundStore
 }
 
-const create = (<T extends object>(
-  createState: StateCreator<T, [], []> | undefined
-) => (createState ? createImpl(createState) : createImpl)) as Create
+const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
+  createState ? createImpl(createState) : createImpl) as Create
 
 export default create
diff --git a/src/vanilla.ts b/src/vanilla.ts
index a172ef4..50c03ba 100644
--- a/src/vanilla.ts
+++ b/src/vanilla.ts
@@ -5,7 +5,7 @@ type SetStateInternal<T> = {
   ): void
 }['_']
 
-export interface StoreApi<T extends object = object> {
+export interface StoreApi<T> {
   setState: SetStateInternal<T>
   getState: () => T
   subscribe: (listener: (state: T, prevState: T) => void) => () => void
@@ -21,7 +21,7 @@ export type Mutate<S, Ms> = Ms extends []
   : never
 
 export type StateCreator<
-  T extends object,
+  T,
   Mis extends [StoreMutatorIdentifier, unknown][] = [],
   Mos extends [StoreMutatorIdentifier, unknown][] = [],
   U = T
@@ -37,17 +37,17 @@ export interface StoreMutators<S, A> {}
 export type StoreMutatorIdentifier = keyof StoreMutators<unknown, unknown>
 
 type CreateStore = {
-  <T extends object, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
+  <T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
     initializer: StateCreator<T, [], Mos>
   ): Mutate<StoreApi<T>, Mos>
 
-  <T extends object>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
+  <T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
     initializer: StateCreator<T, [], Mos>
   ) => Mutate<StoreApi<T>, Mos>
 }
 
 type CreateStoreImpl = <
-  T extends object,
+  T,
   Mos extends [StoreMutatorIdentifier, unknown][] = []
 >(
   initializer: StateCreator<T, [], Mos>
@@ -74,9 +74,10 @@ const createStoreImpl: CreateStoreImpl = (createState) => {
         : partial
     if (nextState !== state) {
       const previousState = state
-      state = replace
-        ? (nextState as TState)
-        : Object.assign({}, state, nextState)
+      state =
+        replace ?? typeof nextState !== 'object'
+          ? (nextState as TState)
+          : Object.assign({}, state, nextState)
       listeners.forEach((listener) => listener(state, previousState))
     }
   }


Test patch:
diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx
index 2c2397c..0549d20 100644
--- a/tests/basic.test.tsx
+++ b/tests/basic.test.tsx
@@ -576,3 +576,25 @@ it('ensures a subscriber is not mistakenly overwritten', async () => {
   expect((await findAllByText('count1: 1')).length).toBe(2)
   expect((await findAllByText('count2: 1')).length).toBe(1)
 })
+
+it('works with non-object state', async () => {
+  const useCount = create(() => 1)
+  const inc = () => useCount.setState((c) => c + 1)
+
+  const Counter = () => {
+    const count = useCount()
+    return (
+      <>
+        <div>count: {count}</div>
+        <button onClick={inc}>button</button>
+      </>
+    )
+  }
+
+  const { getByText, findByText } = render(<Counter />)
+
+  await findByText('count: 1')
+
+  fireEvent.click(getByText('button'))
+  await findByText('count: 2')
+})