I'm trying to create a class which allows passing a callback to alter the side-effects of a method. If you don't pass a callback, then the method will be called directly. This is a basic example:
class Button<T = void> {
private clickWrapper?: (click: Function) => T
private _click() {
// do the click here
return null;
}
constructor(clickWrapper?: (click: Function) => T) {
this.clickWrapper = clickWrapper;
}
public click() {
if (this.clickWrapper) {
return this.clickWrapper(this._click.bind(this));
} else {
return this._click();
}
}
}
class Foo {
public doStuff() {
console.log('hello');
}
}
const button = new Button<Foo>(click => {
// do some stuff
click();
return new Foo();
});
const foo = button.click();
foo.doStuff();
const button2 = new Button();
button2.click();
This works, but foo.doStuff() complains that foo may be null - even though in this case I provided a clickWrapper, so the return value of button.click() cannot be null, it must be an instance of Foo. Is there a better way to define this?
The second issue is I have to copy the Button constructor's parameter type when I've already declared it for Button.clickWrapper. How do I avoid having to declare the type on the private property and constructor parameter?
I have updated you code snippet:
class Button<T = null> {
constructor(private clickWrapper?: (click: Function) => T) {}
private _click() {
// do the click here
return null;
}
public click(): T {
if (this.clickWrapper) {
return this.clickWrapper(this._click.bind(this));
} else {
return this._click();
}
}
}
class Foo {
public doStuff() {
console.log("hello");
}
}
const button = new Button<Foo>(click => {
// do some stuff
click();
return new Foo();
});
const foo = button.click();
foo.doStuff();
const button2 = new Button();
button2.click();
Two things:
TypeScript can't be sure what is exact return type of your public click function so it assumes T | null, since default _click function returns null
To avoid redeclaring types for constructor and property of an object, you can always use shorthand syntax for constructor assignment (just add private or public keyword to constructor param)
interface Callback<V> {
(arg: () => void): V
}
class Button<T = void> {
constructor(private callback?: Callback<T>) {}
private onClick = () => {
}
public click = () => {
if (this.callback) {
return this.callback(this.onClick)
} else {
return this.onClick()
}
}
}
const button = new Button<number>(
click => {
click()
return 2 +2
}
)
console.log(button.click()) // 4
I update your code to solve your problems
Create an interface for the callback type and add the private callback? to the constructor to inject the argument to the class
There are many types for a function, in typescript a function that not return nothing is a void function, you are returning null, so that didn't match with your clickWrapper type, I assume you aren't gonna return anything from the click function so I update that type to match too with a void function
Related
I'm trying to create a TypeScript decorator that can be added to any class, that will create a request-scoped context that I can use to store a request ID. I've come across a couple of articles around decorators but none seem to fit my use-case.
Below is the code that I use to create the async hook, the decorator and a sample class that should be wrapped. When running the code, the actual response I receive is an empty object. The context is being lost but I'm not following why. I'm not receiving any errors or warnings.
Any suggestions would be highly appreciated.
This is the code I'm using to create the context. Calling the initContext() and getContext() functions.
import * as asyncHooks from 'async_hooks'
const contexts: any = {}
asyncHooks.createHook({
init: (asyncId: any, type: any, triggerAsyncId: any) => {
if (contexts[triggerAsyncId]) {
contexts[asyncId] = contexts[triggerAsyncId]
}
}
}).enable()
function initContext(fn: any) {
const asyncResource = new asyncHooks.AsyncResource('REQUEST_CONTEXT')
return asyncResource.runInAsyncScope(() => {
const asyncId = asyncHooks.executionAsyncId()
contexts[asyncId] = {}
return fn(contexts[asyncId])
})
}
function getContext() {
const asyncId = asyncHooks.executionAsyncId()
return contexts[asyncId] || {}
}
export { initContext, getContext }
This is the decorator that I'm using to wrap the class with. I'm trying to create the constructor within the context of the initContext function, set the context ID and then return the new constructor.
import { initContext } from './lib/context'
function AsyncHooksContext<T extends { new(...args: any[]): {} }>(target: T) {
return initContext((context: any) => {
context.id = 'some-uuid-goes-here'
return class extends target {
constructor(...args: any[]) {
super(...args)
}
}
})
}
export { AsyncHooksContext }
This is a sample class that should be able to produce the context ID
#AsyncHooksContext
class Foo {
public bar() {
const context = getContext()
console.log('This should be the context => ', { context })
}
}
new Foo().bar()
It seems that if I just rename public events: Eventing = new Eventing(); in my User class to anything other than events it would stop working. I understand that as long as the name of that property matches with the name of the property in eventing.ts, it works. Why ? That is what I want to find out.
stackblitz
index.ts
import { User } from "./user";
const user = new User();
const on = user.on;
user.on("change", () => {
alert("yo");
});
user.trigger("change");
user.ts
import { Eventing } from "./eventing";
export interface UserProps {
id?: number;
name?: string;
age?: number;
}
export class User {
public events: Eventing = new Eventing(); //stops working if renamed to anything other than events
constructor() {}
get on() {
return this.events.on;
}
get trigger() {
return this.events.trigger;
}
}
eventing.ts
type Callback = () => void;
export class Eventing {
events: { [key: string]: Callback[] } = {};
on(eventName: string, callback: Callback): void {
const handlers = this.events[eventName] || [];
handlers.push(callback);
this.events[eventName] = handlers;
}
trigger(eventName: string): void {
const handlers = this.events[eventName];
if (!handlers || handlers.length === 0) {
return;
}
handlers.forEach(callback => {
callback();
});
}
}
EDIT:
this inside User class refers to User class itself. But since User class has a property called event that is an instance of Eventing class, when I try to execuet this.event.on() it must execute, right? Why won't it execute? Why does the property name has to match with the one in Eventing class?
It's happening because you are binding user this to on and trigger methods here:
get on() {
return this.events.on;
}
get trigger() {
return this.events.trigger;
}
So inside on and trigger methods, this will be instance of User and not Eventing.
You could turn on and trigger in lamba function or just bind methods like this (e.g. you want to change property to foos):
public foos: Eventing = new Eventing();
get on() {
return this.events.on.bind(this.foos);
}
get trigger() {
return this.events.trigger.bind(this.foos);
}
I have these two methods which are almost similar:
private firstFunction () {
this.serviceOne.methodOne().subscribe(
res => {
return resultOne = res;
},
err => {}
);
}
private secondFunction () {
this.serviceTwo.methodTwo().subscribe(
res => {
return resultTwo = res;
},
err => {}
);
}
I want to write a generic function, like this:
genericFunction (service ,method , result ) {
service.method().subscribe(
res => {
return result = res;
},
err => {}
);
}
And consequently I want to get something like this working:
genericFunction (serviceOne , methodOne , resultOne );
genericFunction (serviceTwo , methodTwo , resultTwo );
Actually, I cannot find how to pass methodOne and methodTwo as params. Any sugestions?
There are several issues in your code.
Firstly, you want to modify the field you pass in as a parameter (as suggested by result = res. You can't pass in a reference to a field, but you can pass in the field name, and use indexing to change the field. keyof T will allow you to pass in the field in a type safe way.
Secondly if you want to access a method on a service. Again we can do this passing in the method name, and we can constrain the service to have a method with the passed in method name, that returns an Observable. The result of the Observable can also be constrained to be of the same type of the field we are going to assign it to in order for the method to be fully type safe.
declare class Service1 {
method1() : Observable<number>
}
declare class Service2 {
method2() : Observable<string>
}
class MyClass {
resultOne!: number;
resultTwo!: string;
constructor() {
this.genericFunction(new Service1(), "method1", "resultOne");
this.genericFunction(new Service2(), "method2", "resultTwo");
this.genericFunction(new Service1(), "method1", "resultTwo"); // error resultTwo is a string, the method return Observable<number>
this.genericFunction(new Service2(), "method", "resultTwo"); // error method does not exit on Service2
this.genericFunction(new Service2(), "method2", "resultTwo2"); // error field does not exist on type
}
genericFunction<MethodKey extends string, ResultKey extends keyof MyClass>(service:Record<MethodKey, ()=> Observable<MyClass[ResultKey]>>, method:MethodKey, result: ResultKey){
service[method]().subscribe(
res => this[result] = res,
err => {}
);
}
}
Note We could have also passed in the function as a function not just as a name, but directly a typed function. The disadvantage of this is that we either have to use bind to ensure the service method will still have the correct this when it's called, or use an arrow function when calling (again to ensure the service method has the correct this). This is error prone though, bind results in an untyped function, so we can't check compatibility to the field, and someone might pass service.method directly and no error would be reported until runtime:
class MyClass {
resultOne!: number;
resultTwo!: string;
constructor() {
var service1 = new Service1()
var service2 = new Service2()
this.genericFunction(()=> service1.method1(), "resultOne");
this.genericFunction(()=> service2.method2(), "resultTwo");
this.genericFunction(service2.method2, "resultTwo"); // no error, depending on the implementation of method2 it might or might not work
this.genericFunction(service2.method2.bind(service2), "resultOne"); // no error, the service call will work, but we store it in an incompatible variable
this.genericFunction(()=> service1.method1(), "resultTwo");// error resultTwo is a string, the method return Observable<number>
this.genericFunction(()=> service2.method2(), "resultTwo2");// // error field does not exist on type
}
genericFunction<MethodKey extends string, ResultKey extends keyof MyClass>(method:()=> Observable<MyClass[ResultKey]>, result: ResultKey){
method().subscribe(
res => this[result] = res,
err => {}
);
}
}
try by using the following code:
private firstFunction () {
let response= genericFunction(this.serviceOne.methodOne())
}
private secondFunction () {
let response = genericFunction(this.serviceTwo.methodTwo())
}
Modify you Generic Function by just receiving a variable.
//if it is angular 4 or less
genericFunction (method: Observable) {
return method.map(res => {
return res.json();
});
}
//if it is angular 5 or 6
genericFunction (method: Observable) {
return method.pipe(
map(res => {
return res;
}));
}
I'm trying to understand how class decorators work in Typescript when we wish to replace the constructor. I've seen this demo:
const log = <T>(originalConstructor: new(...args: any[]) => T) => {
function newConstructor(... args) {
console.log("Arguments: ", args.join(", "));
new originalConstructor(args);
}
newConstructor.prototype = originalConstructor.prototype;
return newConstructor;
}
#log
class Pet {
constructor(name: string, age: number) {}
}
new Pet("Azor", 12);
//Arguments: Azor, 12
Everything is understood but this line:
newConstructor.prototype = originalConstructor.prototype;
Why do we do that?
The classes like:
class Pet {
constructor(name: string, age: number) {}
dosomething() {
console.log("Something...");
}
}
Are compiled into functions when targeting ES5:
var Pet = (function () {
function Pet(name, age) {
}
Pet.prototype.dosomething = function () {
console.log("Something...");
};
return Pet;
}());
As you can seem when we use functions to define classes. The methods are added to the function's prototype.
This means that if you are going to create a new constructor (new function) you need to copy all the methods (the prototype) from the old object:
function logClass(target: any) {
// save a reference to the original constructor
const original = target;
// a utility function to generate instances of a class
function construct(constructor: any, args: any[]) {
const c: any = function () {
return constructor.apply(this, args);
};
c.prototype = constructor.prototype;
return new c();
}
// the new constructor behaviour
const newConstructor: any = function (...args: any[]) {
console.log("New: " + original.name);
return construct(original, args);
};
// copy prototype so intanceof operator still works
newConstructor.prototype = original.prototype;
// return new constructor (will override original)
return newConstructor;
}
You can learn more at "Decorators & metadata reflection in TypeScript: From Novice to Expert (Part I)"
Update
Please refer to https://github.com/remojansen/LearningTypeScript/tree/master/chapters/chapter_08 for a more recent version.
I have this simple class:
class Foo {
constructor() {
this.init();
return this;
}
init() {
this.onInit();
}
onInit(callback) {
this.onInit = () => callback();
return this;
}
}
new Foo().onInit(() => console.log('baz'));
It's obviously flawed, because it will call init before the onInit method is able to define the onInit property/callback.
How can I make this work without change the interface?
How can I make this work without change the interface?
You can't, the interface is inherently flawed. That's really the answer to your question.
Continuing, though, with "what can I do instead":
If you need to have a callback called during initialization, you need to pass it to the constructor, not separately to the onInit method.
class Foo {
constructor(callback) {
this.onInit = () => {
callback(); // Call the callback
return this; // Chaining seemed important in your code, so...
};
// Note: Constructors don't return anything
}
}
new Foo(() => console.log('baz'));
In a comment you've said:
I see your point, the fact is that my library is new Something().onCreate().onUpdate()
It sounds like you might want to adopt the builder pattern instead:
class Foo {
constructor(callbacks) {
// ...use the callbacks here...
}
// ...
}
Foo.Builder = class {
constructor() {
this.callbacks = {};
}
onCreate(callback) {
this.callbacks.onCreate = callback;
}
onUpdate(callback) {
this.callbacks.onUpdate = callback;
}
// ...
build() {
// Validity checks here, do we have all necessary callbacks?
// Then:
return new Foo(this.callbacks);
}
};
let f = new Foo.Builder().onCreate(() => { /*...*/}).onUpdate(() => { /*... */}).build();
...although to be fair, a lot of the advantages (though not all) of the builder pattern can be realized in JavaScript by just passing an object into constructor directly and doing your validation there, e.g.:
let f = new Foo({
onCreate: () => { /*...*/},
onUpdate: () => { /*...*/}
});
Assuming that onInit is supposed to be some sort of hook to be called synchronously whenever an object is instantiated, you can't solve this on the instance level.
You can make onInit a static function, like so:
class Foo {
constructor() {
// whatever
Foo.onInit();
}
static onInit() {} // empty default
}
Foo.onInit = () => console.log('baz'); // Override default with your own function
const f = new Foo();
const f2 = new Foo();