Search code examples
typescriptclasscachinggetter

Whats's the best way to cache a getter (without decorators)?


The practice of "caching" a getter is a way to only call a value one time and reuse that same value without recomputing. I'm looking for the "best" way to cache a getter, by best I mean with the shortest syntax, with typing. There are many libraries out there that simply allow you to use a decorator like @cache over the getter and it behaves the way I've described, i'm not looking to use decorators.

Typescript playground

Here's one way:

type NoUndefined<T> = T extends undefined ? never : T;
function isNoUndefined <T> (value: T): value is NoUndefined<T> {
  return typeof value !== 'undefined'
}
function handleGetter <T>(value:T, operation: () => NoUndefined<T>): NoUndefined<T> {
  if (isNoUndefined(value)) return value;
  return operation()
}

class CalendarDate extends Date {
  #day: number | undefined = undefined
  get day () {
    return this.#day = handleGetter(this.#day, () => {
      console.log('runs once')
      return this.getDate()
    })
  }
}

const c = new CalendarDate()

c.day
c.day
c.day
c.day

Solution

  • One possible approach is to write a function which replaces a getter in an existing class prototype with a new one that wraps it with a smart/self-overwriting/lazy getter:

    function cacheGetter<T>(ctor: { prototype: T }, prop: keyof T): void {
      const desc = Object.getOwnPropertyDescriptor(ctor.prototype, prop);
      if (!desc) throw new Error("OH NO, NO PROPERTY DESCRIPTOR");
      const getter = desc.get;
      if (!getter) throw new Error("OH NO, NOT A GETTER");
      Object.defineProperty(ctor.prototype, prop, {
        get() {
          const ret = getter.call(this);
          Object.defineProperty(this, prop, { value: ret });
          return ret;
        }
      })
    }
    

    So if you call cacheGetter(Clazz, "key"), it will get the property descriptor for Clazz.prototype.key, and make sure it has a getter. If either step fails it throws an error. Otherwise it makes a new getter that, when called, calls the original getter once on the current instance (not the prototype), and then defines the property directly (again, not the prototype) on the instance as the cached value. So the next time the property is accessed on the instance, it uses the instance cached value and not the inherited getter.


    Let's test it. You apply it after the class is declared:

    class CalendarDate extends Date {
      get day() {
        console.log('runs once');
        return this.getDate();
      }
    }
    cacheGetter(CalendarDate, "day"); // <-- here
    

    And make sure it works as expected:

    const c = new CalendarDate()
    console.log(c.day); // runs once, 25
    console.log(c.day); // 25
    console.log(c.day); // 25
    console.log(c.day); // 25
    
    
    const d = new CalendarDate();
    d.setDate(10);
    console.log(d.day) // runs once, 10
    console.log(c.day) // 25
    console.log(d.day) // 10
    

    Looks good.


    So that's the shortest method I can think of for users if you don't want to use decorators. I assume that the decorator approach would be implemented in a similar way, as a wrapper for the property descriptor.

    Playground link to code