how to memoize a TypeScript getter - node.js

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.

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

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! Must have been having a good day. Thanks to Yeshua my friend and Saviour :)

Related

TS decorator to wrap function definition in try catch

Is it possible to use TS decorator to wrap a function definition into a try-catch block. I don't want to use try-catch in every function so I was thinking maybe decorators can help.
For example
function examleFn(errorWrapper: any) {
try{
// some code
} catch (err) {
errorWrapper(err)
}
}
Something like this can be done in a decorator so that it can be used for other functions too.
No, you cannot decorate functions.
TypeScript's implementation of decorators can only apply to classes, class methods, class accessors, class properties, or class method parameters. The relevant proposal for JavaScript decorators (at Stage 3 of the TC39 Process as of today, 2022-07-21) also does not allow for decorating functions.
Function decorators are mentioned as possible extensions to the decorator proposal, but are not currently part of any proposal for either TypeScript or JavaScript.
You can, of course, call a decorator-like function on another function, but this is just a higher-order function and not a decorator per se, and it won't affect the original function declaration:
const makeErrorWrapper = <T,>(errorHandler: (err: any) => T) =>
<A extends any[], R>(fn: (...a: A) => R) =>
(...a: A): R | T => {
try {
return fn(...a);
} catch (err) {
return errorHandler(err);
}
};
The makeErrorWrapper function takes an error handler and returns a new function that wraps other functions with that error handler:
const errToUndefined = makeErrorWrapper(err => undefined);
So now errToUndefined is a function wrapper. Let's say we have the following function which throws errors:
function foo(x: string) {
if (x.length > 3) throw new Error("THAT STRING IS TOO LONG");
return x.length;
}
// function foo(x: string): number
If you call it directly, you can get runtime errors:
console.log(foo("abc")); // 3
console.log(foo("abcde")); // šŸ’„ THAT STRING IS TOO LONG
Instead you can wrap it:
const wrappedFoo = errToUndefined(foo);
// const wrappedFoo: (x: string) => number | undefined
Now wrappedFoo is a new function that behaves like foo and takes the same parameter list as foo, but returns number | undefined instead of just number:
console.log(wrappedFoo("abc")) // 3
console.log(wrappedFoo("abcde")) // undefined
Playground link to code
maybe this can help you, it took me a long time to do it, but here it is
function Execpetion (methodName: string) {
return (target: any, nameMethod: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
try {
const executionMethod = await originalMethod.apply(this, args)
return executionMethod
} catch (error) {
return errorWrapper(error as Error)
}
}
}
}
in your class
class TestController {
#Execpetion('TestController')
public async handler (teste: any) {
return {
statusCode: 200,
data: 'nothing'
}
}
}
with the parent function, you can modify and add to receive the errorPersonalized and instantiated type parameter... and on the return put it

Typescript: Generic type of method params to match type of callback function params

I'm trying to make a class that accepts a function in the constructor. The function can have arguments of any type. Then I want to put a method on the class that accepts that same arguments as function parameter, as it will be a wrapper around this callback. Here's a simplified example to show what I'm trying to do
interface Options<T> {
callbackFn(...x: any[]) => Promise<T>
}
class ExampleClass<T> {
private options: Options<T>;
result: T;
constructor(options: Options<T>) {
this.options = options;
}
async wrapperFn(...x: any[]) {
// Do some stuff before the callback
this.result = await this.options.callbackFn(x)
// Do some stuff after
}
}
const example = new ExampleClass<string>({
callbackFn: (a: string, b:string) => new Promise((res) => {
res(a + b);
})
});
example.wrapperFn("foo", "bar")
This is basically the way I have it now, and it works but it obviously doesn't enforce the types of the params of wrapperFn which isn't ideal. Is there any way to do something like this?
If you want the compiler to keep track of both the callback return type and the callback argument list type, then you'll want Options to be generic in both the return type (you called it T but I'll call it R for "return") and the argument list type (I'll call it A for "arguments"):
interface Options<A extends any[], R> {
callbackFn(...x: A): Promise<R>
}
Now you can just use A anywhere you were using any[] before, and you'll get stronger typing. This also implies that ExampleClass needs to be generic in A and R too:
class ExampleClass<A extends any[], R> {
private options: Options<A, R>;
result?: R;
constructor(options: Options<A, R>) {
this.options = options;
}
async wrapperFn(...x: A) {
// Do some stuff before the callback
this.result = await this.options.callbackFn(...x)
// Do some stuff after
}
}
Let's test it out:
const example = new ExampleClass({
callbackFn: (a: string, b: string) => new Promise<string>((res) => {
res(a + b);
})
});
// const example: ExampleClass<[a: string, b: string], string>
example.wrapperFn("foo", "bar") // okay
example.wrapperFn("foo", 123); // error!
// --------------------> ~~~
// Argument of type 'number' is not assignable to parameter of type 'string'.
Looks good.
Playground link to code

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

It's not possible to mock classes with static methods using jest and ts-jest

I have two classes that simulate a simple sum operation.
import SumProcessor from "./SumProcessor";
class Calculator {
constructor(private _processor: SumProcessor) { }
sum(a: number, b: number): number {
return this._processor.sum(a, b)
}
}
export default Calculator
And the operation processor.
class SumProcessor {
sum(a: number, b: number): number {
return a + b
}
static log() {
console.log('houston...')
}
}
export default SumProcessor
I'm tryng to mock the class SumProcessor to write the following unit test using jest+ts-jest.
import Calculator from "./Calculator"
import SumProcessor from "./SumProcessor"
import { mocked } from "ts-jest/utils"
jest.mock('./SumProcessor')
describe('Calculator', () => {
it('test sum', () => {
const SomadorMock = <jest.Mock>(SumProcessor)
SomadorMock.mockImplementation(() => {
return {
sum: () => 2
}
})
const somador = new SomadorMock()
const calc = new Calculator(somador)
expect(calc.sum(1, 1)).toBe(2)
})
})
When the static method is present in class SumProcessor, the mock code const SomadorMock = (SumProcessor) indicates the following compilation error:
TS2345: Argument of type '() => jest.Mock<any, any>' is not assignable to parameter of type '(values?: object, option
s?: BuildOptions) => SumOperator'.
Type 'Mock<any, any>' is missing the following properties from type 'SumOperator...
If the static method is removed from SumProcessor class, everything work's fine.
Can anybody help?
since you have already mocked the SumProcessor class with jest.mock('./SumProcessor'); you can just add a spy to the method you would like to mock, for an example:
jest.spyOn(SumProcessor.prototype, 'sum').mockImplementation(() => 2);
this way your test class would look something like this:
import Calculator from "./Calculator"
import SumProcessor from "./SumProcessor"
jest.mock('./SumProcessor')
describe('Calculator', () => {
it('test sum', () => {
jest.spyOn(SumProcessor.prototype, 'sum').mockImplementation(() => 2);
const somador = new SumProcessor();
const calc = new Calculator(somador)
expect(calc.sum(1, 1)).toBe(2)
})
})
much simpler, right?

Symbol to string with JSON.stringify

I need to convert a Symbol to string in order to create a unique key in Redis, but I can't.
I've already tried to use Object.toString(obj) and String(obj) but I get errors or [Object] resultsĀ”.
This is the controller
const name = req.params.name;
let obj;
obj.data.name = {
[Op.like]: '%' + name + '%'
};
}
This is redis controller where I use stringify. I use obj as a parameter.
const hashed = crypto.createHmac('sha256', secretHashKey)
.update(JSON.stringify(obj))
.digest('hex');
I expect an output based on my parameter 'obj' but now it's not getting it so I can't create unique keys for different values.
Maybe a little bit too late, but I hope that somebody else find this useful.
I was looking for something exactly as you: use with Sequelize in a Redis cache.
Mine is TypeScript, convert to JavaScript just by removing the typings.
export function JsonStringifyWithSymbols(object: any, clean?: boolean): string {
return JSON.stringify(object, (_, value) => {
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
const props = [...Object.getOwnPropertyNames(value), ...Object.getOwnPropertySymbols(value)];
const replacement: Record<string, any> = {};
for (const k of props) {
if (typeof k === 'symbol') {
replacement[`Symbol:${Symbol.keyFor(k)}`] = value[k];
} else {
replacement[k] = value[k];
}
}
return replacement;
}
return value;
});
}
If you're meaning these Symbols you can't convert them to a string.
They're created to be unique and "unreversable", so you can use them also for keep more "secure" various properties or methods. Example:
const a = Symbol('a')
class Foobar {
constructor (_a) {
this[a] = _a
}
}
const foobar = new Foobar('aaa')
console.log(foobar) // output: Foobar { [Symbol(a)]: 'aaa' }
const fake = Symbol('a')
foobar[fake] = 'fake'
console.log(foobar) // output: Foobar { [Symbol(a)]: 'aaa', [Symbol(a)]: 'fake' }
You can't corrupt the original one, unless you have the original Symbol.
Another example (info about the JSON.stringify here):
const a = Symbol('a')
const foobar = {}
foobar[a] = 'aaa'
console.log(foobar) // output: { [Symbol(a)]: 'aaa' }
console.log(JSON.stringify(foobar)) // output: {}
const fake = Symbol('a')
foobar[fake] = 'fake'
console.log(foobar) // output: { [Symbol(a)]: 'aaa', [Symbol(a)]: 'fake' }
Hope these info will help you.

Resources