I want to create a wrapper function for an existing function in TypeScript.
The wrapper function could start some other process and clean it up after finishing the main ("callback") function passed to the wrapper.
This can be done using approaches like shown here. However, these solutions do not allow me to specify additional options that can be passed to the wrapper itself.
How would I go about doing that?
My starting point was:
export const wrap = async <T>(
callback: () => T | Promise<T>,
options?: { foo?: string | undefined },
): Promise<T> => {
let ret;
// begin
if (options.foo) {
// do something
}
try {
ret = await callback();
} catch (e) {
throw e;
} finally {
// cleanup
}
return ret;
};
This would not let me add arguments to callback(). I can use ...args, but how would I specify both ...args and options?
I suggest you to write your function as a factory (function that returns another function):
function wrap<T extends (...args: any[]) => any>(callback: T, options?: { foo?: string | undefined }): (...args: Parameters<T>) => ReturnType<T> extends Promise<infer U> ? Promise<U> : Promise<ReturnType<T>>
function wrap(callback: (...args: any[]) => any, options?: { foo?: string | undefined }): (...args: any[]) => Promise<any> {
return async function (...args: any[]) {
let ret;
// begin
if (options && options.foo) {
// do something
}
try {
ret = await callback(...args);
} catch (e) {
throw e;
} finally {
// cleanup
}
return ret;
}
};
async function asyncExtractFirstParameter(str: string, num: number, bool: boolean) {
return str;
}
function extractFirstParameter(str: string, num: number, bool: boolean) {
return str;
}
const wrappedAsyncExtractFirstParameter = wrap(asyncExtractFirstParameter);
const wrappedExtractFirstParameter = wrap(extractFirstParameter);
wrappedAsyncExtractFirstParameter('test', 23, true).then(console.log);
wrappedExtractFirstParameter('test', 23, true).then(console.log);
That allows you to create another function with the same signature as the callback, and possibly defer its execution.
TypeScript playground
The solution consists of making the functon generic (F) and inferring the parameters and return types of that function via ReturnType and Parameters. Any additional options can be specified in advance of the destructured remaining arguments which are passed to the callback function:
export const wrap = async <F extends (...args: any[]) => ReturnType<F> | Promise<ReturnType<F>>>(
callback: F,
options: { foo?: string | undefined } = {},
...args: Parameters<F>
): Promise<ReturnType<F>> => {
let ret: ReturnType<F> | undefined;
// begin
if (options.foo) {
// do something
}
try {
ret = await callback(...args);
} catch (e) {
throw e;
} finally {
// cleanup
}
return ret;
};
The wrapper can then be called like this:
const someFunction = (arg1: string, arg2: boolean): void => {
// do something
}
wrap(someFunction, { foo: "yes" }, arg1, arg2)`
All return types and argument types are inferred automatically.
Related
Currently, I have an angular application with a method decorator to handle errors of all the methods in the component. It catches all the errors from methods, But it is unable to catch the error inside subscribe. Any suggestions to do this?
This is my current code.
This is the sampling method I want to catch the errors
#logActionErrors()
getEmailSettings() {
this.sharedService.getSMTPConfigurations().subscribe(() => {
throw('this is an error');
}, (ex) =>{
console.log(ex);
})
}
This is my Method Decorator
export function logActionErrors(): any {
return function (target: Function, methodName: string, descriptor: any) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
try {
let result = method.apply(this, args);
// Check if method is asynchronous
if (result && result instanceof Promise) {
// Return promise
return result.catch((error: any) => {
handleError(error, methodName, args, target.constructor.name);
});
}
if(result && result instanceof Observable ){
console.log(methodName);
result.pipe(catchError((error: any) => {
console.log(error);
handleError(error, methodName, args, this.constructor.name);
return result
}))
}
// Return actual result
return result;
} catch (error:any) {
handleError(error, methodName, args, target.constructor.name);
}
}
return descriptor;
}
}
I want to catch this throw('this is an error'); error on this sample. any suggestions to do this?
Imagine you have to change method arguments at runtime, using a decorator. A trivial example to make it simple: all arguments being set to "Hello World":
export const SillyArguments = (): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = (...args: any[]) => {
Object.keys(args).forEach(i => {
args[i] = 'Hello World';
});
return originalMethod.apply(null, args);
};
return descriptor;
}
};
Example usage:
class TestClass {
private qux = 'qux';
#SillyArguments()
foo(val: any) {
console.log(val);
console.log(this.qux);
this.bar();
}
bar() {
console.log('bar');
}
}
const test = new TestClass();
test.foo('Ciao mondo'); // prints "Hello World"
TypeError: Cannot read property 'qux' of null
The problem here is apply(null, args), which changes the context of this. This makes impossible to call the instance variable named qux, from inside foo().
Another possibility is to change the call to originalMethod.apply(target, args), but this time qux is undefined, while bar() can be invoked.
Is there any possibility to invoke the originalMethod with the context of this correctly set to the instance?
Use a function function instead of an arrow function so that you receive the original this context and can pass it along:
export const SillyArguments = (): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
Object.keys(args).forEach(i => {
args[i] = 'Hello World';
});
return originalMethod.apply(this, args);
};
return descriptor;
}
};
I have a class decorator and would like to use it to apply another decorator to all methods within the class, but I am not quite sure how to apply a decorator programmatically without the # syntax:
#LogAllMethods
class User {
greet() {
console.log('Hello')
}
}
function LogAllMethods(target) {
for (const key in Object.keys(target.prototype)) {
// apply LogMethod decorator
}
}
function LogMethod(target, key, descriptor) {
let method = descriptor.value
decriptor.value = function(...args) {
console.log(args)
method.apply(this, ...args)
}
}
You basically just have to call the decorator function with the target, the key (method name) and the defined descriptor:
function LogAllMethods<T>(target: new (...params: any[]) => T) {
for (const key of Object.getOwnPropertyNames(target.prototype)) {
let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);
descriptor = LogMethod(target.prototype, key, descriptor);
if (descriptor) {
Object.defineProperty(target.prototype, key, descriptor);
}
}
}
function LogMethod(target: any, key: symbol | string, descriptor: TypedPropertyDescriptor<any> = undefined) {
if (descriptor) {
let method = descriptor.value;
if (method instanceof Function) {
descriptor.value = function (...args: any[]) {
console.log("Log", args)
method.apply(this, ...args);
}
}
return descriptor;
} else {
// descriptor is null for target es5 if the decorator is applied directly to the merhod
let method = target[key];
if (method instanceof Function) {
target[key] = function (...args: any[]) {
console.log("Log", args)
method.apply(this, ...args);
}
}
}
}
I have a structure similar to this:
class Person {
greet() {
console.log(this.constructor.name)
}
}
class User extends Person {
}
let user = new User()
user.greet()
Unfortunately it prints window for this.constructor.name instead of User.
Is there some other way to get the actual class Name?
Actual code:
static MeteorMethod(target: MeteorModel, key: string, descriptor: PropertyDescriptor) {
let constructor = target.constructor
let className = target.constructor.name
let methodName = key
let method = descriptor.value
let meteorMethodName = MeteorModelDecorators.generateMethodName(constructor, methodName)
MeteorModelDecorators.MeteorMethodClasses[className] = target
if(Meteor.isServer) {
Meteor.methods({
[meteorMethodName]: (modelClassName: string, modelProps: object, ...args: any[]) => {
let model = new MeteorModelDecorators.MeteorMethodClasses[modelClassName](modelProps)
method.apply(model, args)
}
})
}
else {
descriptor.value = async function(...args: any[]) {
// here I expect this to be Book, but I get Window
return new Promise(function(resolve, reject) {
Meteor.call(meteorMethodName, this.constructor.name, this, args, (error: any, result: any) => {
if(error) reject(error)
resolve(result)
})
})
}
}
}
class MeteorModel {
#MeteorMethod
save() {
console.log('save')
}
}
class Book extends MeteorModel {
}
let book = new Book()
book.save()
Your problem is in this part:
descriptor.value = async function(...args: any[]) {
// here I expect this to be Book, but I get Window
return new Promise(function(resolve, reject) {
Meteor.call(meteorMethodName, this.constructor.name, this, args, (error: any, result: any) => {
if(error) reject(error)
resolve(result)
})
})
}
It needs to be this:
descriptor.value = async function(...args: any[]) {
// With the arrow function, should be Book
return new Promise((resolve, reject) => {
Meteor.call(meteorMethodName, this.constructor.name, this, args, (error: any, result: any) => {
if(error) reject(error)
resolve(result)
})
})
}
The function you were passing to the Promise constructor was setting up a new context, by using the arrow function you pick up the this context from the surrounding method.
I am trying to write a small cache wrapper in typescript (simplified pseudo demo code):
const cache = {};
export function cachify<T, V>(name:string, getFunction: (i:V)=>Promise<T>): (i:V) => Promise<T> {
return function() {
return cache[name] || getFunction.apply(this,arguments)
}
})
This works great if my function has only one argument e.g.
function isNameFancy(name:string) {
return Promise.resolve(true)
}
const isNameFancyWithCache = cachify(isNameFancy)
However as i specified i:V this only valid for one argument.
If I have a second function e.g. isPersonFancy it won't work:
function isPersonFancy(personAge: number, personName: string) {
return Promise.resolve(true)
}
const isPersonFancyWithCache = cachify(isPersonFancy)
How do I have to change my cachify function types so that it works for both cases?
You can declare other signatures for the cachify function:
const cache = {};
export function cachify<T, V>(name: string, getFunction: (i: V) => Promise<T>): (i: V) => Promise<T>;
export function cachify<T, V1, V2>(name: string, getFunction: (i: V1, j: V2) => Promise<T>): (i: V1, j: V2) => Promise<T>;
export function cachify(name: string, getFunction: (...args: any[]) => Promise<any>): (...args: any[]) => Promise<any> {
return function () {
return cache[name] || getFunction.apply(this,arguments)
}
};
function isNameFancy(name: string) {
return Promise.resolve(true)
}
const isNameFancyWithCache = cachify("isNameFancy", isNameFancy); // (i: string) => Promise<boolean>
function isPersonFancy(personAge: number, personName: string) {
return Promise.resolve(true)
}
const isPersonFancyWithCache = cachify("isPersonFancy", isPersonFancy); // (i: number, j: string) => Promise<boolean>
(code in playground)