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

TypeScript type conflict with persist middleware #650

Closed
eduardohley opened this issue Nov 10, 2021 · 17 comments · Fixed by #651
Closed

TypeScript type conflict with persist middleware #650

eduardohley opened this issue Nov 10, 2021 · 17 comments · Fixed by #651

Comments

@eduardohley
Copy link

I am unable to get the persist middleware working with TypeScript if I define state properties with multiple types or with the boolean type.

zustand version: 3.6.4
TypeScript version: 4.1.3

Example code:

import create, { GetState, SetState } from "zustand";
import { persist, StoreApiWithPersist } from "zustand/middleware";

type AuthStore = {
  token: string | undefined;
  authenticated: boolean;
  authenticate: (username: string, password: string) => Promise<void>;
}

const useAuthStore = create<AuthStore, SetState<AuthStore>, GetState<AuthStore>, StoreApiWithPersist<AuthStore>>(
  persist(
    (set) => ({
      token: undefined,
      authenticated: false,
      authenticate: async (username, password) => {
        set({authenticated: true})
      },
    }),
    { name: "auth-store" }
  )
);

export default useAuthStore;

persist function gives the following errors:

 Type 'string | undefined' is not assignable to type 'undefined'.
          Type 'string' is not assignable to type 'undefined'.ts(2345)

 The types of 'authenticated' are incompatible between these types.
        Type 'boolean' is not assignable to type 'false'.ts(2345)

Everything works as expected if I just define token and authenticated as any type.

@dai-shi
Copy link
Member

dai-shi commented Nov 10, 2021

Thanks for reporting.
With v3.6.5, if you don't need the persist api types, you should be able to do this:

const useAuthStore = create<AuthStore>(
  persist(
    (set) => ({
      token: undefined,
      authenticated: false,
      authenticate: async (username, password) => {
        set({authenticated: true})
      },
    }),
    { name: "auth-store" }
  )
);

If you need a custom store api type, it's a bit tricker. see #632 and:

// NOTE: Adding type annotation to inner middleware works.

@shkreios
Copy link

Hey @dai-shi I tried the solution you posted, but I still run into the type problem. The problem just came up for me when I upgrade from 3.6.2 to 3.6.5. The workaround with setting the persist API types seems to work. I am on typescript 4.4.4.

@dai-shi
Copy link
Member

dai-shi commented Nov 10, 2021

Thanks for chiming in. Hmm, I thought I tested in tests. As you say, it can repro in csb:
https://codesandbox.io/s/agitated-cohen-g8r4f?file=/src/App.tsx

🤔 🤔 🤔

@dai-shi
Copy link
Member

dai-shi commented Nov 10, 2021

My misunderstanding was #641 doesn't work for this union type issue.
This issue is related with #631 and #632.

The only workaround I came up with is #651.

@dai-shi
Copy link
Member

dai-shi commented Nov 11, 2021

I nailed down the issue in typescript playground.

@dai-shi
Copy link
Member

dai-shi commented Nov 11, 2021

Here's another workaround:

    type MyState = {
      token: string | undefined
      authenticated: boolean
      authenticate: (username: string, password: string) => Promise<void>
    }
    type MyPersist = (
      config: StateCreator<MyState>,                                            
      options: PersistOptions<MyState>                                          
    ) => StateCreator<MyState>                                                  
    const useStore = create<MyState>(                                           
      (persist as MyPersist)(                                                   
        (set) => ({                                                             
          token: undefined,                                                     
          authenticated: false,                                                 
          authenticate: async (_username, _password) => {                       
            set({ authenticated: true })                                        
          },                                                                    
        }),                                                                     
        { name: 'auth-store' }                                                  
      )                                                                         
    )

Basically, we type cast to that of v3.6.2 which didn't have persist api.
I will modify #651 to show this as a possible workaround.

@eduardohley
Copy link
Author

Thank you @dai-shi I like that workaround

@dai-shi dai-shi linked a pull request Nov 11, 2021 that will close this issue
@DavFeng1
Copy link

Any workaround for the subscribeWithSelector middleware?

@dai-shi
Copy link
Member

dai-shi commented Jan 24, 2022

I think I tried to cover various patterns in middlewareTypes.test.tsx for workarounds. What's a missing combination?
(We would like to fix it completely in v4. Working with/by @devanshj)

@enzoferey
Copy link

enzoferey commented Apr 27, 2022

Here a pattern of store that uses a normal store slice + a persist store slice in TypeScript in case this is helpful for somebody. I struggled to get the type rights in order to be able to use the store via the vanilla API (useStore.getState()) as well. The solution works but there is no type inference unfortunately, you need to be explicit about the persisting store slices types.

import { createConfigurationStoreSlice } from "./configuration/store";
import { createWalletStoreSlice } from "./wallet/store";

// A Store slice can read its state and the state of other slices but can only set its own state
export type StoreSlice<T extends object, E extends object = T> = (
  set: SetState<T>,
  get: GetState<E extends T ? E : E & T>
) => T;

type PersistStoreApiType = ReturnType<typeof createWalletStoreSlice>;

const createRootSlice = (
  set: SetState<any>,
  get: GetState<any>,
  api: Mutate<
    StoreApi<PersistStoreApiType>,
    [["zustand/persist", Partial<PersistStoreApiType>]]
  >
) => {
  return {
    ...createConfigurationStoreSlice(set, get),
    ...persist(createWalletStoreSlice, {
      name: "wallet",
      partialize: (state) => {
        return {
          lastConnectedWalletLabel: state.lastConnectedWalletLabel,
        };
      },
    })(set, get, api),
  };
};

export const useStore = create(createRootSlice);

If you have any other store slice that you want to persist, you can add it to the PersistStoreApiType type like:

type PersistStoreApiType = 
  ReturnType<typeof createFirstStoreSlice> &
  ReturnType<typeof createSecondStoreSlice> &
  ReturnType<typeof createThirdStoreSlice>;

@crashtech
Copy link

Here's another workaround:

    type MyState = {
      token: string | undefined
      authenticated: boolean
      authenticate: (username: string, password: string) => Promise<void>
    }
    type MyPersist = (
      config: StateCreator<MyState>,                                            
      options: PersistOptions<MyState>                                          
    ) => StateCreator<MyState>                                                  
    const useStore = create<MyState>(                                           
      (persist as MyPersist)(                                                   
        (set) => ({                                                             
          token: undefined,                                                     
          authenticated: false,                                                 
          authenticate: async (_username, _password) => {                       
            set({ authenticated: true })                                        
          },                                                                    
        }),                                                                     
        { name: 'auth-store' }                                                  
      )                                                                         
    )

Basically, we type cast to that of v3.6.2 which didn't have persist api. I will modify #651 to show this as a possible workaround.

Expanding on this one, this version allows you to access the persist method and get things like rehydrate().

    type MyState = {
      token: string | undefined
      authenticated: boolean
      authenticate: (username: string, password: string) => Promise<void>
    }

    type PersistResult<
      T = MyState,
      U = Partial<MyState>,
      Mps extends [StoreMutatorIdentifier, unknown][] = [],
      Mcs extends [StoreMutatorIdentifier, unknown][] = [],
    > = StateCreator<T, Mps, [['zustand/persist', U], ...Mcs]>
    
    type MyPersist = (
      config: StateCreator<MyState>,
      options: PersistOptions<Partial<MyState>>,
    ) => MyPersist

    const useStore = create<MyState>(                                           
      (persist as MyPersist)(                                                   
        (set) => ({                                                             
          token: undefined,                                                     
          authenticated: false,                                                 
          authenticate: async (_username, _password) => {                       
            set({ authenticated: true })                                        
          },                                                                    
        }),                                                                     
        { name: 'auth-store' }                                                  
      )                                                                         
    )

@josealvarez97
Copy link

josealvarez97 commented Mar 24, 2023

Here's another workaround:

    type MyState = {
      token: string | undefined
      authenticated: boolean
      authenticate: (username: string, password: string) => Promise<void>
    }
    type MyPersist = (
      config: StateCreator<MyState>,                                            
      options: PersistOptions<MyState>                                          
    ) => StateCreator<MyState>                                                  
    const useStore = create<MyState>(                                           
      (persist as MyPersist)(                                                   
        (set) => ({                                                             
          token: undefined,                                                     
          authenticated: false,                                                 
          authenticate: async (_username, _password) => {                       
            set({ authenticated: true })                                        
          },                                                                    
        }),                                                                     
        { name: 'auth-store' }                                                  
      )                                                                         
    )

Basically, we type cast to that of v3.6.2 which didn't have persist api. I will modify #651 to show this as a possible workaround.

Life saver!

There was no way I could fix the following error message without (persist as MyPersist).

(Excuse the application specific types in the error message below. There's nothing sensitive about such types—we're just coding a sort of game about immigration—so I won't bother removing them. Just in case anyone encounters a similar error with a similar pattern. It called my attention that many of my types showed up as never[] when in fact they were supposed to be string[])

Argument of type '(set: (partial: (GlobalStoreState & GlobalStoreActions) | Partial<GlobalStoreState & GlobalStoreActions> | ((state: GlobalStoreState & GlobalStoreActions) => (GlobalStoreState & GlobalStoreActions) | Partial<...>), replace?: boolean | undefined) => void, get: () => GlobalStoreState & GlobalStoreActions) => { ...; }' is not assignable to parameter of type 'StateCreator<{ answers: {}; timeCost: number; legalCost: number; factorsHistory: never[]; countriesHistory: never[]; defaultAnswers: { US_Relatives: string; US_Government_Affiliation: string; last_US_Entry: string; }; ... 13 more ...; initializingPrevailingWageData: MutableRefObject<...>; }, [...], [], { ...; }>'.
  Type '(set: (partial: (GlobalStoreState & GlobalStoreActions) | Partial<GlobalStoreState & GlobalStoreActions> | ((state: GlobalStoreState & GlobalStoreActions) => (GlobalStoreState & GlobalStoreActions) | Partial<...>), replace?: boolean | undefined) => void, get: () => GlobalStoreState & GlobalStoreActions) => { ...; }' is not assignable to type '(setState: (partial: { answers: {}; timeCost: number; legalCost: number; factorsHistory: never[]; countriesHistory: never[]; defaultAnswers: { US_Relatives: string; US_Government_Affiliation: string; last_US_Entry: string; }; ... 13 more ...; initializingPrevailingWageData: MutableRefObject<...>; } | Partial<...> | ...'.
    Types of parameters 'set' and 'setState' are incompatible.
      Types of parameters 'partial' and 'partial' are incompatible.
        Type '(GlobalStoreState & GlobalStoreActions) | Partial<GlobalStoreState & GlobalStoreActions> | ((state: GlobalStoreState & GlobalStoreActions) => (GlobalStoreState & GlobalStoreActions) | Partial<...>)' is not assignable to type '{ answers: {}; timeCost: number; legalCost: number; factorsHistory: never[]; countriesHistory: never[]; defaultAnswers: { US_Relatives: string; US_Government_Affiliation: string; last_US_Entry: string; }; ... 13 more ...; initializingPrevailingWageData: MutableRefObject<...>; } | Partial<...> | ((state: { ...; }) =>...'.
          Type 'GlobalStoreState & GlobalStoreActions' is not assignable to type '{ answers: {}; timeCost: number; legalCost: number; factorsHistory: never[]; countriesHistory: never[]; defaultAnswers: { US_Relatives: string; US_Government_Affiliation: string; last_US_Entry: string; }; ... 13 more ...; initializingPrevailingWageData: MutableRefObject<...>; } | Partial<...> | ((state: { ...; }) =>...'.
            Type 'GlobalStoreState & GlobalStoreActions' is not assignable to type 'Partial<{ answers: {}; timeCost: number; legalCost: number; factorsHistory: never[]; countriesHistory: never[]; defaultAnswers: { US_Relatives: string; US_Government_Affiliation: string; last_US_Entry: string; }; ... 13 more ...; initializingPrevailingWageData: MutableRefObject<...>; }>'.
              Types of property 'factorsHistory' are incompatible.
                Type 'string[]' is not assignable to type 'never[]'.
                  Type 'string' is not assignable to type 'never'.

@LordEvendim
Copy link

Here's another workaround:

    type MyState = {
      token: string | undefined
      authenticated: boolean
      authenticate: (username: string, password: string) => Promise<void>
    }
    type MyPersist = (
      config: StateCreator<MyState>,                                            
      options: PersistOptions<MyState>                                          
    ) => StateCreator<MyState>                                                  
    const useStore = create<MyState>(                                           
      (persist as MyPersist)(                                                   
        (set) => ({                                                             
          token: undefined,                                                     
          authenticated: false,                                                 
          authenticate: async (_username, _password) => {                       
            set({ authenticated: true })                                        
          },                                                                    
        }),                                                                     
        { name: 'auth-store' }                                                  
      )                                                                         
    )

Basically, we type cast to that of v3.6.2 which didn't have persist api. I will modify #651 to show this as a possible workaround.

Thanks for the workaround!

For anyone having an issue with the partialize, here is a slightly modified solution to fix it.

    type MyState = {
      token: string | undefined
      authenticated: boolean
      authenticate: (username: string, password: string) => Promise<void>
    }
    type MyPersist = (
      config: StateCreator<MyState>,                                            
      options: PersistOptions<MyState, Partial<MyState>>                                          
    ) => StateCreator<MyState>                                                  
    const useStore = create<MyState>(                                           
      (persist as MyPersist)(                                                   
        (set) => ({                                                             
          token: undefined,                                                     
          authenticated: false,                                                 
          authenticate: async (_username, _password) => {                       
            set({ authenticated: true })                                        
          },                                                                    
        }),                                                                     
        { name: 'auth-store' }                                                  
      )                                                                         
    )

@amerllica
Copy link

I prefer to use // @ts-ignore rather than many typing codes which they could be written by Zustand developers

@itayperry
Copy link
Contributor

itayperry commented May 15, 2023

Hi there @dai-shi, thank you!! Your solution helped me ✨
Are there any plans for the future (type-wise) that will not require applying these solutions?
I'm currently using "zustand": "^4.3.8" and "typescript": "^4.9.5"

@dai-shi
Copy link
Member

dai-shi commented May 15, 2023

Looks like this issue is old. I think typing is fixed/improved since then. Please open a new discussion with typescript playground reproduction.

@itayperry
Copy link
Contributor

I'm so sorry for wasting your time, I used TypeScript without adding the extra parenthesis :(

I should've read the docs :)
The thing is, I used the non-TypeScript version in TypeScript and my editor went nuts. I ,mistakenly thought it was a bug.

https://docs.pmnd.rs/zustand/guides/typescript

"The difference when using TypeScript is that instead of writing create(...), you have to write create<T>()(...) (notice the extra parenthesis () too along with the type parameter) where T is the type of the state to annotate it"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants