5

I am using the following approach to memoize a TypeScript getter using a decorator but wanted to know if there is a better way. I am using the popular memoizee package from npm as follows:

import { memoize } from '@app/decorators/memoize'

export class MyComponent {

  @memoize()
  private static memoizeEyeSrc(clickCount, maxEyeClickCount, botEyesDir) {
    return clickCount < maxEyeClickCount ? botEyesDir + '/bot-eye-tiny.png' : botEyesDir + '/bot-eye-black-tiny.png'
  }

  get leftEyeSrc() {
    return MyComponent.memoizeEyeSrc(this.eyes.left.clickCount, this.maxEyeClickCount, this.botEyesDir)
  }
}

AND the memoize decorator is:

// decorated method must be pure
import * as memoizee from 'memoizee'

export const memoize = (): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const func = descriptor.value
    descriptor.value = memoizee(func)
    return descriptor
  }
}

Is there a way to do this without using two separate functions in MyComponent and to add the decorator directly to the TypeScript getter instead?

One consideration here is that the decorated function must be pure (in this scenario) but feel free to ignore that if you have an answer that doesn't satisfy this as I have a general interest in how to approach this problem.

2
  • But how decorator would decide if cached version should be used? You'll need to mark somehow which members affect the getter Commented May 15, 2019 at 12:19
  • 1
    yes, I understand, thanks for comment - however, this was only an optional requirement to this question - see my comment to @estus answer below Commented May 15, 2019 at 12:25

2 Answers 2

6

The decorator can be extended to support both prototype methods and getters:

export const memoize = (): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    if ('value' in descriptor) {
      const func = descriptor.value;
      descriptor.value = memoizee(func);
    } else if ('get' in descriptor) {
      const func = descriptor.get;
      descriptor.get = memoizee(func);
    }
    return descriptor;
  }
}

And be used directly on a getter:

  @memoize()
  get leftEyeSrc() {
    ...
  }
Sign up to request clarification or add additional context in comments.

6 Comments

But in this case cache will be never invalidated, right?
brilliant answer - will try it out when I get a chance and accept if it works - this doesn't help when using memoize since the decorated function must be pure and getters often aren't pure - but generally this is a wonderful approach (if it works which I am sure it will) - many thanks :)
@AlekseyL. Yes. If you need to memoize it depending on memoizeEyeSrc arguments, of course you need a separate function. But you can still can make descriptor.get = a wrapper around memoizee to decide whether it needs to be invalidated based on this properties.
Another thing - it seems that decorated getter won't be able to access other instance members (this.*)
@AlekseyL. It can be accessed, it's a common case , descriptor.get = function () { /* this is class instance here */ }.
|
0

Based on @estus answer, this is what I finally came up with:

@memoize(['this.eyes.left.clickCount'])
get leftEyeSrc() {
  return this.eyes.left.clickCount < this.maxEyeClickCount ? this.botEyesDir + '/bot-eye-tiny.png' : this.botEyesDir + '/bot-eye-black-tiny.png'
}

And the memoize decorator is:

// decorated method must be pure when not applied to a getter

import { get } from 'lodash'
import * as memoizee from 'memoizee'

// noinspection JSUnusedGlobalSymbols
const options = {
  normalizer(args) {
    return args[0]
  }
}

const memoizedFuncs = {}

export const memoize = (props: string[] = []): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    props = props.map(prop => prop.replace(/^this\./, ''))
    if ('value' in descriptor) {
      const valueFunc = descriptor.value
      descriptor.value = memoizee(valueFunc)
    } else if ('get' in descriptor) {
      const getFunc = descriptor.get
      // args is used here solely for determining the memoize cache - see the options object
      memoizedFuncs[propertyKey] = memoizee((args: string[], that) => {
        const func = getFunc.bind(that)
        return func()
      }, options)
      descriptor.get = function() {
        const args: string[] = props.map(prop => get(this, prop))
        return memoizedFuncs[propertyKey](args, this)
      }
    }
    return descriptor
  }
}

This allows for an array of strings to be passed in which determine which properties will be used for the memoize cache (in this case only 1 prop - clickCount - is variable, the other 2 are constant).

The memoizee options state that only the first array arg to memoizee((args: string[], that) => {...}) is to be used for memoization purposes.

Still trying to get my head around how beautiful this code is!

1 Comment

IMO path navigation with Lodash is overkill, that it disables type checking is a big disadvantage for TS. In case you need this behaviour, original approach is much more straightforward. The use of memoizeEyeSrc is justified. It shouldn't necessarily be a part of the class, could be just a helper function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.