danday74
danday74

Reputation: 57106

how to memoize a TypeScript getter

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.

Upvotes: 5

Views: 9839

Answers (2)

danday74
danday74

Reputation: 57106

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!

Upvotes: 0

Estus Flask
Estus Flask

Reputation: 222750

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() {
    ...
  }

Upvotes: 6

Related Questions