Class composition in TypeScript - javascript

I want to build a class that can compose multiple objects and use any of their interfaces.
Class A can use any of the interfaces of Class B and C
B can use any of the interfaces of C
C can use any of the interfaces of B
I have the above functionality written in JavaScript and I was wondering what's the best and correct way to achieve the same using TypeScript:
import { findLast, isFunction } from "lodash";
class Composite {
constructor(behavior) {
this.behaviors = [];
if (behavior) {
this.add(behavior);
}
}
add(behavior) {
behavior.setClient(this);
this.behaviors.push(behavior);
return this;
}
getMethod(method) {
const b = findLast(this.behaviors, (behavior) =>
isFunction(behavior[method])
);
return b[method].bind(b);
}
}
class Behavior1 {
foo() {
console.log("B1: foo");
}
foo2() {
console.log("B1: foo2");
this.getMethod("bar")();
}
setClient(client) {
this.client = client;
}
getMethod(method) {
return this.client.getMethod(method);
}
}
class Behavior2 {
foo() {
console.log("B2: foo");
this.getMethod("foo2")();
}
bar() {
console.log("B2: bar");
}
setClient(client) {
this.client = client;
}
getMethod(method) {
return this.client.getMethod(method).bind(this);
}
}
const c = new Composite();
c.add(new Behavior1());
c.add(new Behavior2());
c.getMethod("foo")();
c.getMethod("bar")();
// Output:
// B2: foo
// B1: foo2
// B2: bar
// B2: bar
Link to codesandbox: https://codesandbox.io/s/zen-poitras-56f4e?file=/src/index.js

You can review my other answer to see some of the issues and concerns with the previous approach. Here I've created a completely different version from the ground up. There is less code repetition and less tight coupling between the classes.
Behaviors no longer call methods directly and no longer store a reference to the client. Instead, they receive the client (or any object which call get and call methods) as an argument of their register method.
We define any object which can lookup and call methods as a MethodAccessor
interface MethodAccessor {
getMethod(name: string): () => void;
safeCallMethod(name: string): boolean;
}
We define any object that provides behaviors through a register method as a BehaviorWrapper. These objects can call functions from other objects by calling getMethod or safeCallMethod on the helper argument.
type KeyedBehaviors = Record<string, () => void>;
interface BehaviorWrapper {
register(helper: MethodAccessor): KeyedBehaviors;
}
A behavior which does not need instance variables could be a pure function rather than a class.
const functionBehavior = {
register(composite: MethodAccessor) {
return {
foo: () => console.log("B1: foo"),
foo2: () => {
console.log("B1: foo2");
composite.safeCallMethod("bar");
}
};
}
};
Class behaviors can make use of instance variables in their methods.
class ClassBehavior {
name: string;
constructor(name: string) {
this.name = name;
}
bar = () => {
console.log(`Hello, my name is ${this.name}`);
};
register() {
return {
bar: this.bar
};
}
}
There is some redundancy here when defining a method like bar separately rather than inline as an arrow function within the return object. The reason that I am having the methods come from register rather than using all class methods is so that I can have stricter typing on them. You could have methods in your class which do require args and as long as they aren't part of the register returned object then it's not a problem.
Our class Composite now stores its behaviors in a keyed object rather than an array. Newly added behaviors of the same name will overwrite older ones. Our getMethod is typed such that it always returns a method, and will throw an Error if none was found. I've added a new method safeCallMethod to call a method by name. If a method was found, it calls it and returns true. If no method was found, it catches the error and returns false.
class Composite implements MethodAccessor {
behaviors: KeyedBehaviors = {};
constructor(behavior?: BehaviorWrapper) {
if (behavior) {
this.add(behavior);
}
}
// add all behaviors from a behavior class instance
add(behavior: BehaviorWrapper): this {
this.behaviors = {
...this.behaviors,
...behavior.register(this)
};
return this;
}
// lookup a method by name and return it
// throws error on not found
getMethod(method: string): () => void {
const b = this.behaviors[method];
if (!b) {
throw new Error(`behavior ${method} not found`);
}
return b;
}
// calls a method by name, if it exists
// returns true if called or false if not found
safeCallMethod(method: string): boolean {
try {
this.getMethod(method)();
return true;
} catch (e) {
return false;
}
}
}

There's a lot that's not ideal about your setup. I might post a separate answer with an alternate setup, but for now I just want to show you how to convert your code to typescript.
Keep in mind that typescript errors exist to help you prevent runtime errors, and there are some genuine potential runtime errors that we need to avoid. If a Behavior calls getMethod before calling setClient to set this.client that will be a fatal error. If you try to call the returned method from getMethod on a Composite or a Behavior where the name didn't match a method that's another fatal error. And so on.
You choose to handle certain situations by throwing an Error with the expectation that it will be caught later on. Here I am preferring to "fail gracefully" and just do nothing or return undefined if we can't do what we want. The optional chaining ?. helps.
When defining an interface for a function argument, it's best to keep it to the minimum necessities and not require any extraneous properties.
The only thing that a Behavior requires of its Client is a getMethod method.
interface CanGetMethod {
getMethod(name: string): MaybeMethod;
}
We use the union of undefined and a void function in a few places, so I am saving it to an alias name for convenience.
type MaybeMethod = (() => void) | undefined;
The Composite calls setClient on its behaviors, so they must implement this interface.
interface CanSetClient {
setClient(client: CanGetMethod): void;
}
It also expects that its methods take zero arguments, but we can't really declare this with the current setup. It is possible to add a string index to a class, but that would conflict with our getMethod and setClient arguments which do require arguments.
One of the typescript errors that you get a bunch is `Cannot invoke an object which is possibly 'undefined', so I created a helper method to wrap a function call.
const maybeCall = (method: MaybeMethod): void => {
if (method) {
method();
}
};
In typescript, classes need to declare the types for their properties. Composite gets an array of behaviors behaviors: CanSetClient[]; while the behaviors get a client client?: CanGetMethod;. Note that the client must be typed as optional because it is not present when calling new().
After that, it's mostly just a matter of annotating argument and return types.
I have declared the interfaces that each class implements, ie. class Behavior1 implements CanGetMethod, CanSetClient, but this is not required. Any object fits the interface CanGetMethod if it has a getMethod property with the right types, whether it explicitly declares CanGetMethod in its type or not.
class Composite implements CanGetMethod {
behaviors: CanSetClient[];
constructor(behavior?: CanSetClient) {
this.behaviors = [];
if (behavior) {
this.add(behavior);
}
}
add(behavior: CanSetClient): this {
behavior.setClient(this);
this.behaviors.push(behavior);
return this;
}
getMethod(method: string): MaybeMethod {
const b = findLast(this.behaviors, (behavior) =>
isFunction(behavior[method])
);
return b ? b[method].bind(b) : undefined;
}
}
class Behavior1 implements CanGetMethod, CanSetClient {
client?: CanGetMethod;
foo() {
console.log("B1: foo");
}
foo2() {
console.log("B1: foo2");
maybeCall(this.getMethod("bar"));
}
setClient(client: CanGetMethod): void {
this.client = client;
}
getMethod(method: string): MaybeMethod {
return this.client?.getMethod(method);
}
}
class Behavior2 implements CanGetMethod, CanSetClient {
client?: CanGetMethod;
foo() {
console.log("B2: foo");
maybeCall(this.getMethod("foo2"));
}
bar() {
console.log("B2: bar");
}
setClient(client: CanGetMethod) {
this.client = client;
}
getMethod(method: string): MaybeMethod {
return this.client?.getMethod(method)?.bind(this);
}
}
const c = new Composite();
c.add(new Behavior1());
c.add(new Behavior2());
maybeCall(c.getMethod("foo"));
maybeCall(c.getMethod("bar"));

Related

Implementing kind of a delegation pattern in Javascript

I have two classes, A and B. What I am trying to do is to pass data from A to B after receiving a message from sockets.
This is simplified look of how classes are defined:
class A:
export default class A {
client;
callbacks;
constructor() {
this.callbacks = {
open: () => this.client.logger.debug('open'),
close: () => this.client.logger.debug('closed'),
message: (data) => {this.client.logger.log(data)}, //I want to pass this data object to class B
};
this.client = new Spot(constants.apiKey, constants.apiSecret, {
baseURL: constants.baseURL,
wsURL: constants.wsURL,
});
this.client.userData(listenKey, this.callbacks);
}
}
I already have a property of A in class definition of B:
export default class B {
account;
constructor() {
this.account = new A();
}
}
What would be a correct/standard way to connect these two so I get a 'data' object from class A every time the socket message callback from class A is triggered?
I am a bit new with JS, but on iOS we would use a delegation pattern, with a protocol, that says:
class A will have a delegate property.
A delegate (class B) must implement a protocol (in this case it would be a requirement to implement method called didReceiveMessage(data).
After that, when a message is received in class A, we would just do(in socket message callback shown above) something like this.delegate.didReceiveMessage(data).
Protocol usage here is not important generally, but it is a plus, cause from A class, we can only access didReceiveData(data) method trough a delegate property, and nothing else (other properties / methods of class B are not visible). At least that is how it works in Swift/Obj-C. I just mentioned it, cause I am curious is this how it is done in JS too.
I guess there is some similar mechanism in Javascript, or some more standard/better way to achieve this kind of data sending between objects?
on iOS we would use a delegation pattern, with a protocol
You can do it exactly as you described:
export default class A {
client;
delegate;
constructor(delegate) {
this.delegate = delegate;
this.client = new Spot(constants.apiKey, constants.apiSecret, {
baseURL: constants.baseURL,
wsURL: constants.wsURL,
});
const callbacks = {
open: () => this.client.logger.debug('open'),
close: () => this.client.logger.debug('closed'),
message: (data) => this.delegate.didReceiveMessage(data),
};
this.client.userData(listenKey, callbacks);
}
}
export default class B {
account;
constructor() {
this.account = new A(this);
}
didReceiveMessage(data) {
console.log(data); // or whatever
}
}
There is no interface (protocol) declaration that would tell A which properties and methods it may access on the passed delegate, but the contract exists of course. You should document it in prose. (Or use TypeScript).
Notice also how your class A interacts with the Spot client, it uses very much the same pattern of passing an object with event handler methods.
A simpler pattern in JavaScript, if you just need a single method in your protocol, is to pass a callable function only:
export default class A {
client;
constructor(onMessage) {
this.client = new Spot(constants.apiKey, constants.apiSecret, {
baseURL: constants.baseURL,
wsURL: constants.wsURL,
});
this.client.userData(listenKey, {
open: () => this.client.logger.debug('open'),
close: () => this.client.logger.debug('closed'),
message: onMessage,
});
}
}
export default class B {
account;
constructor() {
this.account = new A(this.didReceiveMessage.bind(this));
// or inline:
this.account = new A(data => {
console.log(data); // or whatever
});
}
didReceiveMessage(data) {
console.log(data); // or whatever
}
}
I am not an expert on NodeJs, but you can use something like an emitter plugin.
In javascript, it would look like this:
function A() {
Emitter(this);
this.action = function() {
console.log("something happened");
this.emit("action", { prop: "value" });
};
}
function B(a_instance) {
// subscribe to "action" event
a.on("action", function(data) {
console.log(data.prop); // "value"
});
};
var myA = new A();
var myB = new B(myA);
myA.action();

Client-side typescript: how do I remove circular dependencies from the factory method pattern?

I am using the factory method pattern in some of my code. The problem is, some of those instances also use the same factory method pattern. This creates circular dependencies and I can't think of a way of removing them. Let me give an example:
// factoryMethod.ts
import Instance1 from './Instance1';
import Instance2 from './Instance2';
import Instance3 from './Instance3';
import { Instance, InstanceName } from './Instance';
export const getInstanceByName = (
instanceName: InstanceName
): Instance => {
switch (instanceName) {
case 'Instance1':
return Instance1;
case 'Instance2':
return Instance2;
case 'Instance3':
return Instance3;
default:
throw new Error();
}
};
// extremelyHelpfulUtilityFunction.ts
import { getInstanceByName } from './factoryMethod';
export const extremelyHelpfulUtilityFunction = (instanceName: InstanceName): number => {
// Imagine this was an extremely helpful utility function
return getInstanceByName(instanceName).num
}
// Instance.ts
export interface Instance {
doSomething: () => number;
num: number;
}
export type InstanceName = 'Instance1' | 'Instance2' | 'Instance3';
// Instance1.ts
import { extremelyHelpfulUtilityFunction } from './extremelyHelpfulUtilityFunction';
const i: Instance = {
doSomething: (): number => {
return extremelyHelpfulUtilityFunction('Instance2') + extremelyHelpfulUtilityFunction('Instance3'); // circular dependency
},
}
export default i;
// Other instances defined below, you get the idea.
I'm using rollup to turn this into a single JavaScript file, and when I do, it warns me that I have a circular dependency. I want to get rid of this warning. I realize the code will still function, but I don't want the warning there. How can I modify this code so that InstanceX can get InstanceY without it being a circular dependency?
IMO the problem is that extremelyHelpfulUtilityFunction has to know getInstanceByName, while the result of this factory could always be known in advance by the caller and the desired value passed as argument to the helper.
I would propose
// Instance1.ts
const instance1: Instance = {
doSomething: (): number => {
return (new Instance2()).toNum() + (new Instance3()).toNum()
},
}
with toNum defined in Instance.ts and overridden in its subclasses, using the helper but with proper parameters, for example
// Instance2.ts
const instance2: Instance = {
doSomething: ...,
toNum: (): number => {
return extremelyHelpfulUtilityFunction(1234)
}
}
where you would use this.num instead of 1234 if you declared a proper class for Instance2 instead of this object, like
// Instance2.ts
class Instance2 extends Instance {
num = 1234;
doSomething: ...
toNum(): number {
return extremelyHelpfulUtilityFunction(this.num)
}
}
export default new Instance2();

Prevent a call to a super method from happening multiple times in a proxy in JavaScript

I've written a TypeScript decorator/JavaScript mixin around a React Component called autodispose. Suppose that class A extends autodispose(Component) and class B extends A. The mixin ensures that A.componentWillUnmount() is called regardless of whether B.componentWillUnmount() calls super.componentWillUnmount() via a proxy.
(The code is in TypeScript, but the question pertains to JavaScript.)
export function autodispose<
T extends Class<React.Component>
>(Target: T) {
const ObservingComponent = class extends Target {
constructor(...args: any[]) {
super(...args);
// ... mixin setup ...
this.componentWillUnmount = new Proxy(this.componentWillUnmount, {
apply: (target, thisArg, fnArgs) => {
Reflect.apply(target, thisArg, fnArgs);
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
this.__mixinCleanup();
},
});
}
componentWillUnmount() {
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
this.__mixinCleanup();
}
private __mixinCleanup() {
// is a no-op if __mixinCleanup() has already been called
// ...
}
};
}
If B calls super.componentWillUnmount() then the proxy will call A's componentWillUnmount() twice--first by Reflect.apply(target, thisArg, fnArgs) and then immediately after that. I need a way to detect if the call to Reflect.apply() has already called super.componentWillUnmount() and prevent the second call.
I considered temporarily overriding super.componentWillUnmount with another Proxy which sets a flag that it's been called, but, unsurprisingly, you can't override super's methods.
If all else fails, I can just make sure that autodispose doesn't get called in the prototype chain twice, but this solution would be more ideal.
You don't need to (temporarily or otherwise) overwrite B's super.componentWillUnmount - you did define A.prototype.componentWillUnmount yourself! You can just set the flag in there:
export function autodispose(Target) {
return class ObservingComponent extends Target {
constructor(...args) {
super(...args);
// ... mixin setup ...
this._hasSuperBeenCalled = false;
let original = this.componentWillUnmount;
this.componentWillUnmount = function(...args) {
this._hasSuperBeenCalled = false;
original.apply(this, args);
if (!this._hasSuperBeenCalled)
throw new Error(`Warning: ${this.constructor.name}.componentWillUnmount forgot its super call`);
};
}
componentWillUnmount() {
this._hasSuperBeenCalled = true;
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
// ...mixin cleanup
}
};
}

Spread class functions

Is there a way to spread the functions of a class into another object? As a contrived example:
class FooBar {
private service: MyService;
constructor(svc: MyService) {
this.service = svc;
}
public foo(): string {
return "foo";
}
public bar(): string {
return "bar"
}
public fooBar(): string {
return "foobar"
}
}
let obj = new FooBar();
export default {
...obj
};
I would want the exported object to contain all the methods of the class FooBar but not the private property service. However, those methods are placed on the prototype object when compiled to javascript so they are not included in the spread operation and the private property is included on the object to it is in the resulting object.
I know I can do this:
export default {
foo: obj.foo.bind(obj),
bar: obj.bar.bind(obj),
fooBar: obj.fooBar.bind(obj),
};
I would like to avoid this if possible as I will have methods from multiple classes to map.
Note: This is to be used for combining GraphQL resolvers into a single object that will be supplied to the graphql function.
I am running my app using ts-node if that makes any difference.
I had a couple of problems going on. First, I was targeting es6 as my output instead of es5. Doing that caused there to be no prototype on the compiled object.
Second, just doing the spread caused the private property service to be included in the exported object. I ended up writing a helper function as alluded to by #Vivick and #AlekseyL.:
function combineResolvers(...resolvers: any[]): any {
let out: { [key: string]: any } = {}
resolvers.forEach(resolver => {
let proto = Object.getPrototypeOf(resolver)
Object.keys(proto)
.filter(key => {
return isFunction(resolver[key]);
}).forEach(key => {
out[key] = resolver[key].bind(resolver)
})
})
return out
}
function isFunction(functionToCheck: any): boolean {
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}
This still has the problem of including any private functions on the resolver classes into the exported object.
I think this might work; using arrow functions for autobinding the methods and spreading it with Object.assign
class FooBar {
private service: MyService;
constructor(svc: MyService) {
this.service = svc;
}
public foo = (): string => {
return "foo";
}
public bar = (): string => {
return "bar";
}
public fooBar = (): string => {
return "foobar";
}
}
export default { ...Object.assign(new FooBar()) };
But maybe you would want to take a look at this before doing that
https://www.charpeni.com/blog/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think

TypeScript: auto-generated dynamic function names

I have some dynamically generated function names in TypeScript. The only way I can use them now is to cast my objects to <any>. Ex.: <any>myInstance.getDataA(). These functions are dynamically generated based on some rules. Based on the same rules I'd like to generate type-definitions for my classes, but I can not make it work.
original.ts
abstract class Original {
dynamics = ['getData', 'setData'];
constructor() {
// I create functions here dynamically
this.dynamics.forEach((key) => {
this[key + this.info] = () => null;
});
}
get info() {
return 'X';
}
}
my-class.ts
class MyClass extends Original {
get info() {
return 'A';
}
}
my-other-class.ts
class MyOtherClass extends Original {
get info() {
return 'B';
}
}
something.ts
const myInstance = new MyClass();
console.log(myInstance.getDataA()); // TS2339: Property getDataA does not exist on type: 'MyClass'
const myOtherInstance = new MyOtherClass();
console.log(myInstance.getDataB()); // TS2339: Property getDataB does not exist on type: 'MyClass'
I would like to automatically generate a definition file to define these dynamic properties.
Ex.:
my-class.def.ts
declare interface MyClass {
getDataA;
setDataA
}
//my-other-class.def.ts
declare interface MyClass {
getDataB;
setDataB
}
But I can not find a syntax for my definition files to make it work. Pls ask me if I was not clear, and pls help if you have any idea!
Edit for 4.1
Using Template literal types and mapped type 'as' clauses we can now do concatenate strings in the type system and create a class that has these properties created dynamically.
function defineDynamicClass<T extends string[]>(...info: T): {
new (): {
[K in T[number] as `get${Capitalize<K>}`]: () => unknown
} & {
[K in T[number] as `set${Capitalize<K>}`]: (value: unknown) => void
} & {
info: T
}
} {
return class {
get info () {
return info;
}
} as any
}
class MyClass extends defineDynamicClass('A', 'B', 'ABAB') {
}
let s =new MyClass();
s.getA();
s.getABAB();
s.setA("")
s.info;
Playground Link
Before 4.1
The within language approach
There is no way to do this within the type system, since we can't perform string manipulation on string literal types. The closest you can get, without external tools, is to create get/set methods that take a string literal type, that will be of the same as that returned by the getInfo method.
function stringLiteralArray<T extends string>(...v: T[]){ return v;}
abstract class Original {
get(name: this['info'][number]) {
return null;
}
set(name: this['info'][number], value: any) {
return null;
}
get info() : string[]{
return [];
}
}
class MyOtherClass extends Original {
get info() {
return stringLiteralArray('A', 'B', 'ABAB');
}
}
class MyClass extends Original {
get info() {
return stringLiteralArray('C', 'D', 'DEDE');
}
}
let s =new MyClass();
s.get('A') // error
s.get('C') // ok
While this approach is not 100% what you want, form our previous discussions the aim was to have full code-completion for the methods, and this approach achieves this. You get errors if you pass in the wrong value and you get a completion list for the string:
The compiler API approach
A second approach would be to create a custom tool that uses the typescript compiler API to parse the ts files, look for classes derived from Original and generates interfaces containing the methods (either in the same file or a different file) , if you are interested in this I can write the code, but it's not trivial, and while the compiler API is stable I don't think the compiler team takes as much care with backward compatibility as they do with the language (in fact this is the exact statement they make in the documentation page).
If you are interested in such a solution, let me know and I can provide it, but I advise against it.

Categories

Resources