championswimmer / vuex-persist

A Vuex plugin to persist the store. (Fully Typescript enabled)
http://championswimmer.in/vuex-persist
MIT License
1.67k stars 116 forks source link

Support typescript classes (de)serialization #175

Open Xylez01 opened 4 years ago

Xylez01 commented 4 years ago

Right now when using for example localStorage objects that are classes (and not interfaces) are not properly restored. Any properties or methods will not exist. Are there any plans to support this?

We've created our own implementation for our project (using type decorators), willing to share and propose a solution/implementation if you are interrested.

notwhoyouthink1 commented 4 years ago

Hey, could you share your fix? im having the same issue and i think vuex-persisted storage also has this issue, cheers!

Xylez01 commented 4 years ago

Sure, although I'm thinking recently to drop it again, as we had few issues with it in the past.

The core of the solution is this:

const typesThatShouldBeRestored: any[] = []
const classNameProperty = '__className'

// Use this to decorate a class that needs to be restored
export function Restore() {
  return function _Restore<T extends { new (...args: any[]): {} }>(constructor: T) {
    typesThatShouldBeRestored.push(constructor)

    return class extends constructor {
      constructor(...args: any[]) {
        super(...args)
        // @ts-ignore
        this[classNameProperty] = constructor.name
      }
    }
  }
}

function canBeRestored(object: any): boolean {
  return object.hasOwnProperty(classNameProperty)
}

// Use this method to restore the typing on an object
export function restoreTyping(object: AppState | any) {
  if (isArray(object)) {
    object.forEach(item => restoreTyping(item))
  }

  for (const property in object) {
    if (!object.hasOwnProperty(property)) {
      continue
    }

    const targetObject = object[property]
    if (isObject(targetObject)) {
      if (canBeRestored(targetObject)) {
        const type = typesThatShouldBeRestored.find(
          // @ts-ignore
          instanceType => instanceType.name === targetObject[classNameProperty]
        )
        object[property] = Object.assign(Object.create(type!.prototype), targetObject)
      } else {
        restoreTyping(targetObject)
      }
    }
  }
}

Decorate all classes that need to be restored like this:

@Restore()
export class MyDataHoldingClassInStorage { ... }

And lastly, modify the restore state method of the vuex persist:

new VuexPersist({
  ...
  restoreState: (key, storage) => {
    let json = storage!.getItem(key) as string
    if (isEmpty(json)) {
      json = '{}'
    }

    try {
      const state = JSON.parse(json)
      restoreTyping(state)
      return state
    } catch {
      return {}
    }
  }
})
douglasjam commented 3 years ago

I managed it by just accessing the Getter instead of the state directly and in the getter cast it to my class object

Store

const CartStore: Module<CartStoreState, any> = {
  namespaced: true,
  state: {
    cart: new Cart(),
  },
  getters: {
    getCart: (state): Cart => {
      if (state.cart instanceof Cart) {
        return state.cart;
      }

      return Object.assign(new Cart(), state.cart);
    },
  },

Vue Component

const cartStore = namespace("cart");

@Component
export default class ProductCardColumn extends ProductCardColumnProps {
  @cartStore.Getter("getCart") cart;