Spread class functions - javascript

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

Related

How to read instance decorators in typescript?

I can create custom decorator using reflect-metadata and it work fine.
Problem is, that I don`t know how to get all instance decorators.
import 'reflect-metadata';
console.clear();
function readTypes() {
const decorator: MethodDecorator = (target, propertyKey, description) => {
const args = Reflect.getMetadata(
'design:paramtypes',
target,
propertyKey
).map(c => c.name);
const ret = Reflect.getMetadata('design:returntype', target, propertyKey);
console.log(`Arguments type: ${args.join(', ')}.`);
console.log(`Return type: ${ret.name}.`);
};
return decorator;
}
class Foo {}
class Bar {
#readTypes()
public fn(a: number, b: string, c: Foo): boolean {
return true;
}
}
const barInstance = new Bar();
I would like to get all functions with decorator #readTypes from barInstance. How can I do it?
See working example:
https://stackblitz.com/edit/decorators-metadata-example-nakg4c
First of all, you aren't writing any metadata, only reading it. If you want to lookup which properties were decorated, then you have to write metadata to those properties.
For simplicity's sake, let's simplify the decorator to:
// It's a best practice to use symbol as metadata keys.
const isReadTypesProp = Symbol('isReadTypesProp')
function readTypes() {
const decorator: MethodDecorator = (target, propertyKey, description) => {
Reflect.defineMetadata(isReadTypesProp, true, target, propertyKey);
};
return decorator;
}
Now when the decorator is used, it's executed when the class is parsed, not when instances are created. This means that target in the decorator function is actually the prototype of the class constructor function.
In other words target is the same object as
Bar.prototype or barInstance.constructor.prototype or barInstance.__proto__.
Knowing that we can loop over all the property names in the prototype object and look up that metadata we set earlier:
function getReadTypesPropsFromInstance(object: {
constructor: {
prototype: unknown;
};
}) {
const target = object.constructor.prototype;
const keys = Object.getOwnPropertyNames(target);
return keys.filter(key => Reflect.getMetadata(isReadTypesProp, target, key));
}
Which now returns the names of the decorated properties:
const barInstance = new Bar();
console.log(getReadTypesPropsFromInstance(barInstance)); // ["fn"]
Working example

Class composition in TypeScript

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"));

Declaring types for properties generated by a TypeScript class decorator?

Let's say I've declared the following class to use as a decorator in TypeScript:
class Property {
static register(...props: Property[]) {
return function(cls: any) {
props.forEach(prop => {
Object.defineProperty(cls.prototype, prop.propertyName, {
get() {
return this[`${prop.propertyName}_`] + ' (auto-generated)';
},
set(value: any) {
this[`${prop.propertyName}_`] = value;
},
});
});
};
}
constructor(private readonly propertyName: string) {}
}
When applied to a class like this:
#Property.register(new Property('myCustomProperty'))
class MyClass {}
The result is a class with an auto-generated property setter/getter named myCustomProperty:
const obj = new MyClass();
obj['myCustomProperty'] = 'asdf';
console.info(obj['myCustomProperty_']); // outputs 'asdf'
console.info(obj['myCustomProperty']); // outputs 'asdf (auto-generated)'
Unfortunately, TypeScript does not seem to have any knowledge about this property at compile time since it is being generated at runtime.
What is the best way to ensure TypeScript recognizes the existence of these auto-generated properties? Ideally everything would be auto generated for me so that I didn't have to maintain any extra types somewhere else.

Typescript: Property does not exist

I'm trying to develop a decorator for REST Api Interfaces in Typescript. Here it is the decorator implementation
export function RemoteResource(params: any): Function {
console.log("RemoteResource.params: ", params);
return function (target: Function) {
//--POST
target.prototype.post = function () {
console.log("----POST");
};
//--GET
target.prototype.retrieve = function () {
console.log("----GET");
};
//--DELETE
target.prototype.remove = function () {
console.log("----DELETE");
};
//--PULL
target.prototype.update = function () {
console.log("----PULL");
};
console.log("RemoteResource.target: ", target);
return target;
}
}
Now, I can use the decorator #RemoteResource and the methods post|retrieve|remove|update are added to the original object prototype correctly.
#RemoteResource({
path: "/foos",
methods: [],
requireAuth: false
})
export class Foo { }
From here, if I execute
let tester = new Foo();
tester.post() //--This prints out "----POST" correctly
I've the log printed out correctly, but I've also have the following error: "Property 'post' does not exist on type 'Foo'."
While I understand why I'm having this error (Foo doesn't have any declared post property) I'm not sure about how to fix it.
Ideally, I would like that the TS compiler understand that the decorator extends the original object adding up those methods.
How can I achieve it? Any ideas?
Thanks!
Since you are adding these methods dynamically at runtime in the decorator, the compiler has no way of knowing that these methods will exist for Foo instances.
You can change that in different ways, for example:
(1) Using an interface and intersection:
interface RemoteResource {
post(): void;
remove(): void;
update(): void;
retrieve(): void;
}
let tester = new Foo() as Foo & RemoteResource;
tester.post(); // no error
(2) Interface and empty methods:
export class Foo implements RemoteResource {
post: () => void;
remove: () => void;
update: () => void;
retrieve: () => void;
}
let tester = new Foo() as Foo & RemoteResource;
tester.post();
Edit
#Robba suggests:
(3) Ignore all type checking
let tester = new Foo() as any;
tester.post();
or
let tester = new Foo();
tester["post"]();

Can you create nested classes in TypeScript?

Is there a way to nest classes in TypeScript. E.g. I'd like to use them like:
var foo = new Foo();
var bar = new Foo.Bar();
In modern TypeScript we have class expressions which you can use to create a nested class. For example you can do the following :
class Foo {
static Bar = class {
}
}
// works!
var foo = new Foo();
var bar = new Foo.Bar();
Here is a more complex use case using class expressions.
It allows the inner class to access the private members of the outer class.
class classX {
private y: number = 0;
public getY(): number { return this.y; }
public utilities = new class {
constructor(public superThis: classX) {
}
public testSetOuterPrivate(target: number) {
this.superThis.y = target;
}
}(this);
}
const x1: classX = new classX();
alert(x1.getY());
x1.utilities.testSetOuterPrivate(4);
alert(x1.getY());
codepen
I couldn't get this to work with exported classes without receiving a compile error, instead I used namespaces:
namespace MyNamespace {
export class Foo { }
}
namespace MyNamespace.Foo {
export class Bar { }
}
If you're in the context of a type declaration file, you can do this by mixing classes and namespaces:
// foo.d.ts
declare class Foo {
constructor();
fooMethod(): any;
}
declare namespace Foo {
class Bar {
constructor();
barMethod(): any;
}
}
// ...elsewhere
const foo = new Foo();
const bar = new Foo.Bar();
This answer is about a seemless nested class implementation in TypeScript which builds on top of #basarat 's answer.
To make the type of the static nested class Bar accessible (as #PeterMoore pointed out), declare the type of the nested class in a namespace. That way, we can use the shortcut Foo.Bar. By moving type typeof Foo.Bar.prototype into a type in a declared namespace, we do not have to repeat the expression.
class Foo {
static Bar = class {
}
}
declare namespace Foo {
type Bar = typeof Foo.Bar.prototype
}
// Now we are able to use `Foo.Bar` as a type
let bar: Foo.Bar = new Foo.Bar()
For static classes, the following implementation might be more elegant. This, however does not work with non-static classes.
class Foo { }
namespace Foo {
export class Bar { }
}
let bar: Foo.Bar = new Foo.Bar()
To export the class, an export statement can be added after the class and the namespace have been declared, e.g. export default Foo or export { Foo }.
To achieve the same with a non-static nested class, see the following example.
class Foo {
Bar = class {
}
}
declare namespace Foo.prototype {
type Bar = typeof Foo.prototype.Bar.prototype
}
let foo: Foo = new Foo()
let bar: Foo.prototype.Bar = new foo.Bar()
I Hope this can be helpful
Able to:
Create a new inner class instance
Access outer class instance/prototype members
Implement interfaces
Use decorators
Use Case
export interface Constructor<T> {
new(...args: any[]): T;
}
export interface Testable {
test(): void;
}
export function LogClassName<T>() {
return function (target: Constructor<T>) {
console.log(target.name);
}
}
class OuterClass {
private _prop1: string;
constructor(prop1: string) {
this._prop1 = prop1;
}
private method1(): string {
return 'private outer method 1';
}
public InnerClass = (
() => {
const $outer = this;
#LogClassName()
class InnerClass implements Testable {
private readonly _$outer: typeof $outer;
constructor(public innerProp1: string) {
this._$outer = $outer;
}
public test(): void {
console.log('test()');
}
public outerPrivateProp1(): string {
return this._$outer._prop1;
}
public outerPrivateMethod1(): string {
return this._$outer.method1();
}
}
return InnerClass;
}
)();
}
const outer = new OuterClass('outer prop 1')
const inner = new outer.InnerClass('inner prop 1');
console.log(inner instanceof outer.InnerClass); // true
console.log(inner.innerProp1); // inner prop 1
console.log(inner.outerPrivateProp1()); // outer prop 1
console.log(inner.outerPrivateMethod1()); // private outer method 1

Categories

Resources