question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How to split store in different files? And the limits of object type inference

See original GitHub issue

Hello,

I would simply like to split a store in different files and especially to make my different store parts (state, getter, actions) able to inherit from interface, abstract classes, etc… Furthermore, splitting parts of a Pinia store seems to break the this usage. As Pinia seems to not really be typed (cause it is only based on object type inference), we just can’t split the different parts of a store.

I really wonder why Pinia didn’t do like vuex-smart-module allowing to have strong typing through classes usage and to use directly the type of each parts of a store without creating redundant Interface types. People easily say, “oh classes usage like in vuex-smart-module is more verbose cause you have to specify generics”, but when you have to use State, Getter or Action TYPE, you clearly don’t want to write/specify n interfaces with k methods for each part a store.

It is common for a lot of projects with strong architecture to have for requirement the need of an abstraction between different parts of different stores. Example two instances of store like: ProjectResourceStore and HomeResourceStore having each one Action inheriting from the same abstract class AResourceActions. What is really useful is that AResourceActions allows having default implementations and specifying a “contract”. ProjectResourceAction and HomeResourceAction will be obligated to implement all non-implemented AResourceActions methods.

What I say above seems to be clearly impossible with Pinia as it has only object type inference.

It’s also important to understand that with classes if I want to have a Type of a store part (State, Getter, Action), I don’t have to manually specify all the properties/methods in a redundant Interface that will be used as a way to have non-infered type. Indeed, with classes, I can directly use the class as a Type.

This problem has already been mentioned here: #343, but my issue just try to expose the problem with other related issues and limitations of Pinia type system. Also, no satisfying code example in the original issue has been provided.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:9
  • Comments:46 (3 by maintainers)

github_iconTop GitHub Comments

18reactions
aparajitacommented, Jan 23, 2022

Just a followup on this. The example provided by @rzym-on may run, but it fails under strict type checking. After a massive amount of sleuthing into the pinia types, I finally figured out how to split up a store into multiple files and preserve complete type checking. Here we go!

First you need to add this utility somewhere in your project. Let’s assume it’s in @store/:

extractStore.ts

import type {
  PiniaCustomStateProperties,
  StoreActions,
  StoreGeneric,
  StoreGetters,
  StoreState
} from 'pinia'
import type { ToRefs } from 'vue'
import { isReactive, isRef, toRaw, toRef } from 'vue'

type Extracted<SS> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> &
  StoreActions<SS>

/**
 * Creates an object of references with all the state, getters, actions
 * and plugin-added state properties of the store.
 *
 * @param store - store to extract the refs from
 */
export function extractStore<SS extends StoreGeneric>(store: SS): Extracted<SS> {
  const rawStore = toRaw(store)
  const refs: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(rawStore)) {
    if (isRef(value) || isReactive(value)) {
      refs[key] = toRef(store, key)
    } else if (typeof value === 'function') {
      refs[key] = value
    }
  }

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return refs as Extracted<SS>
}

Now create a directory for the store. Let’s assume it’s @store/foo. These files go in the directory:

state.ts

export interface State {
  foo: string
  bar: number
}

export const useState = defineStore({
  id: 'repo.state',

  state: (): State => {
    return {
      foo: 'bar',
      bar: 7
    }
  }
})

getters.ts

import { computed } from 'vue'
import { useState } from './state'

export const useGetters = defineStore('repo.getters', () => {
  const state = useState()

  const foobar = computed((): string => {
    return `foo-${state.foo}`
  })

  const doubleBar = computed((): string => {
    return state.bar * 2
  })

  return {
    foobar,
    doubleBar
  }
})

actions.ts

import { useState } from './state'

export const useActions = defineStore('repo.actions', () => {
  const state = useState()

  function alertFoo(): void {
    alert(state.foo)
  }

  function incrementBar(amount = 1) {
    state.bar += amount
  }

  // Note you are free to define as many internal functions as you want.
  // You only expose the functions that are returned.
  return {
    alerttFoo,
    incrementBar
  }
})

Now you can bring all of the pieces together like this:

import { extractStore } from '@store/extractStore'
import { defineStore } from 'pinia'
import { useActions } from './actions'
import { useGetters } from './getters'
import { useState } from './state'

export const useFooStore = defineStore('foo', () => {
  return {
    ...extractStore(useState()),
    ...extractStore(useGetters()),
    ...extractStore(useActions())
  }
})

Boom! Completely typed and nicely factored. You would then import it like this:

import { useFooStore } from '@store/foo'

const store = useFooStore()
let foo = store.foo // 'bar'
foo = store.foobar // 'foo-bar'
let bar = store.doubleBar // 14
store.incrementBar(3) // store.bar === 10
bar = store.doubleBar // 20

Enjoy! 😁

3reactions
diadalcommented, Apr 15, 2022

you can separate it simple

Screen Shot 2022-04-15 at 10 11 11 AM

actions.ts

import { axiosInstance, appClient } from '../../boot/axios';
import { Regs, Sget, User, UserKey } from '../../components/models/model';
import { UserDat } from '../../components/models/acclist';
import { RepUser } from '../../components/models/StoreInterFace';

import { useState } from './state';
import { defineStore } from 'pinia';



const REGISTER_ROUTE = '/register';
const LOGIN_ROUTE = '/login';
const AUTH_LOGOUT = '/logout';


const empty = UserDat;

export const useActions = defineStore('auth.actions', () => {
  const state = useState();

  async function register(data: Regs) {
    try {
      const r = await axiosInstance.post(REGISTER_ROUTE, data);
      return r.data;
    } catch (error) {
     
      return false;
    }
  }

  async function login(data: Regs) {
    try {
      const response: {
        data: { status: boolean; data: { user: User; data: string } };
      } = await axiosInstance.post(LOGIN_ROUTE, data);
      const mainData = response?.data?.data;
      const user = mainData?.user;
      if (user) {
        state.user = user;
        const to: string = mainData.data;
        axiosInstance.defaults.headers.common = {
          'content-type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
          'M-Version': process.env.APP_VERSION,
          'M-Client': appClient,
          Authorization: `Bearer ${to}`,
        };
        return <RepUser>{ status: true };
      }
      const rep = <RepUser>(
        (response?.data?.data ? response?.data?.data : response?.data)
      );
      return rep;
    } catch (ee) {
      return false;
    }
  }

  function firstlogin(data: { user: User; data: string }) {
    if (data?.user) {
      const newdata = data.user;
      state.user = newdata;
      const to: string = data.data;
      axiosInstance.defaults.headers.common = {
        'content-type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
        Authorization: `Bearer ${to}`,
      };
      return true;
    }
  }

  function updateuser(data: { tok: string; user: User }) {
    const newdata = data.user;
    state.user = newdata;
    return true;
  }

  function userUpdate(data: { id: string; value: boolean | string }) {
    const updateSate = <UserKey>(<unknown>state.user);
    updateSate[data.id] = data.value;
    return true;
  }

  function fetch() {
  
  }

  async function applogout() {
   
  }

  async function logout() {
    
  }
  return {
    register,
    login,
    updateuser,
    userUpdate,
    firstlogin,
    fetch,
    applogout,
    logout,
  };
});

getters.ts

import { defineStore } from 'pinia';
import { computed } from 'vue';
import { User } from '../../components/models/model';
import { useState } from './state';

export const useGetters = defineStore('auth.getters', () => {
  const state = useState();

  const user = computed((): User => {
    return state.user;
  });

  const loggedIn = computed((): boolean => {
    return state.user.login;
  });

  const check = (roles: string | string[]) => {
    const user = state.user;
    if (user && user.roleNames.length > 0) {
      if (Array.isArray(roles) && roles.length) {
        for (const role of roles) {
          if (!user.roleNames.includes(String(role))) {
            return false;
          }
        }
      } else if (roles) {
        const nrole: string = <string>roles;
        if (!user.roleNames.includes(nrole)) {
          return false;
        }
      }
      return true;
    }
    return false;
  };

  return {
    user,
    loggedIn,
    check,
  };
});

index.ts

import { extractStore } from '../extractStore';
import { defineStore } from 'pinia';
import { useActions } from './actions';
import { useGetters } from './getters';
import { useState } from './state';

export const useAuthStore = defineStore('auth', () => {
  return {
    ...extractStore(useState()),
    ...extractStore(useGetters()),
    ...extractStore(useActions()),
  };
});

state.ts

import { defineStore } from 'pinia';
import { UserDat } from '../../components/models/acclist';
import { State } from '../../components/models/StoreInterFace';

export const useState = defineStore({
  id: 'auth.state',
  state: (): State => {
    return {
      user: UserDat,
    };
  },
});

extractStore.ts

import type {
  PiniaCustomStateProperties,
  StoreActions,
  StoreGeneric,
  StoreGetters,
  StoreState,
} from 'pinia';
import type { ToRefs } from 'vue';
import { isReactive, isRef, toRaw, toRef } from 'vue';

type Extracted<SS> = ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> &
  StoreActions<SS>;

/**
 * Creates an object of references with all the state, getters, actions
 * and plugin-added state properties of the store.
 *
 * @param store - store to extract the refs from
 */
export function extractStore<SS extends StoreGeneric>(
  store: SS
): Extracted<SS> {
  const rawStore = toRaw(store);
  const refs: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(rawStore)) {
    if (isRef(value) || isReactive(value)) {
      refs[key] = toRef(store, key);
    } else if (typeof value === 'function') {
      refs[key] = value;
    }
  }

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return refs as Extracted<SS>;
}

StoreInterFace.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import { Store } from 'pinia';
import { Freg, Regs, User } from './model';

export type PiniaActionAdaptor<
  Type extends Record<string, (...args: any) => any>,
  StoreType extends Store
> = {
  [Key in keyof Type]: (
    this: StoreType,
    ...p: Parameters<Type[Key]>
  ) => ReturnType<Type[Key]>;
};

export type PiniaGetterAdaptor<GettersType, StoreType extends Store> = {
  [Key in keyof GettersType]: (
    this: StoreType,
    state: StoreType['$state']
  ) => GettersType[Key];
};

export type State = {
  user: User;
};

export type Getters = {
  user: User;
  loggedIn: boolean;
  check: (roles: string | string[]) => boolean;
};

export interface RepUser {
  status: boolean;
  data: { user: User; data: string };
}

export type Actions = {
  register?: (data: Regs) => Promise<number | string | Freg> | boolean;
  loggedIn?: () => boolean;
  check?: (roles: string | string[]) => boolean;
  login?: (data: Regs) => Promise<false | RepUser>;
  logout?: () => Promise<boolean>;
  applogout?: () => boolean;
  passwordForgot?: (data: string[]) => Promise<boolean | Freg>;
  passwordReset?: (data: string[]) => Promise<boolean | Freg>;
  updateuser?: (data: { id: string; user: User }) => boolean;
  userUpdate?: (data: { id: string; value: boolean | string }) => boolean;
  firstlogin?: (data: { user: User; data: string }) => boolean;
  newfetch?: () => Promise<boolean | string | User>;
  fetch?: () => boolean;
  user?: () => User;
};

export type AuthStore = Store<'auth', State, Getters, Actions>;
Read more comments on GitHub >

github_iconTop Results From Across the Web

Type Inference for Static Compilation ... - Jean-Baptiste Jeannin
We present a type system and inference algorithm for a rich subset of JavaScript equipped with objects, structural subtyping, prototype inheritance, and first- ......
Read more >
A typed chain: exploring the limits of TypeScript
A few notes: It's implemented using an ES6 class and the native map and reduce functions. The sum method closes the chain. The...
Read more >
65 Type Inference on Executables
on binary code type inference, a challenging task that aims to infer typed ... be reused to store different stack frames and memory...
Read more >
Type inference - Ballerina language
Type inference is local and restricted to a single expression. Overuse of type inference can make the code harder to understand.
Read more >
Java 10 Local Variable Type Inference - Oracle for Developers
var productInfo = new Object() { String name = "Apple"; int total = 30; }; System.out.println("name = " + productInfo.name + ", total...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found