Dynamically calling a static method - javascript

In a class there are several static methods and the method to be called will be decided on the run time. How could I call this method dynamically?
export default class channel {
// METHOD THAT WILL DYNAMICALLY CALL OTHER STATIC METHODS
private static methodMap = {
'channel-create' : 'create',
'channel-user-count' : 'userCount',
'channel-close' : 'close'
};
public static do(commandType:string,params: any) {
if(channel.methodMap.hasOwnProperty(commandType)) {
// GET NAME OF THE METHOD
let method = channel.methodMap[commandType];
// CALL METHOD ON THE FLY
//return channel.call(method,params);
// channel.userCount(params);
}
}
/**
* Adds channel to available channel list
*/
private static create(channelName:string) {
}
/**
* Returns count of users in the channel
*/
private static userCount(channelName:string) {
}
}

You can dynamically invoke a method by using Classname['methodName'](param). As in your case, you can invoke create method as Channel['create']('MyChannel')
Here is the working example: Typescript Playground
class Channel {
private static methodMap = {
'channel-create' : 'create',
'channel-user-count' : 'userCount',
'channel-close' : 'close'
};
private static create(channelName:string) {
alert('Called with ' + channelName);
}
private static userCount(channelName:string) {
alert('Usercount called with ' + channelName);
}
public static do(commandType: string, params: any) {
if(Channel.methodMap.hasOwnProperty(commandType)) {
let method = Channel.methodMap[commandType];
return Channel[method](params);
}
}
}
Channel.do('channel-create', 'MyChannel');
Channel.do('channel-user-count', 1000);
Edit: Even though the above approach works, As #Ryan mentioned in his answer, providing functions directly in map is much cleaner.
private static methodMap: MethodMap = {
'channel-create': Channel.create,
'channel-user-count': Channel.userCount,
'channel-close': Channel.close,
};

Store the functions directly in the map:
type MethodMap = { [name: string]: (any) => void };
private static methodMap: MethodMap = {
'channel-create': Channel.create,
'channel-user-count': Channel.userCount,
'channel-close': Channel.close,
};
public static do(commandType: string, params: any) {
if (channel.methodMap.hasOwnProperty(commandType)) {
const method = channel.methodMap[commandType];
method(params);
}
}

To add to the answer by #HardikModha, you can also get the compiler to check the commandType against the possible values:
public static do(commandType: keyof typeof Channel.methodMap, params: any) {
if(Channel.methodMap.hasOwnProperty(commandType)) {
let method = Channel.methodMap[commandType];
return Channel[method](params);
}
}
...
Channel.do('channel-create', 'MyChannel'); // fine
Channel.do('channel-created', 'MyChannel'); // error
(code in playground)

Related

How to avoid two switch?

I have a class with two private methods:
export class LayerEditor {
public layerManager: LayerManager;
constructor() {
this.layerManager = new LayerManager(this);
}
private executeCommand() {
switch (this.layerManager.editableLayer.type) {
case LayerType.common:
this.commandManager.set(new SelectExistCommand(this.reonMap));
break;
case LayerType.point:
this.commandManager.set(new SelectExistPointCommand(this.reonMap));
break;
}
}
private createTools(): EditorTools {
switch (this.layerManager.editableLayer.type) {
case LayerType.common:
return new CommonTools(new CommonToolsFactory());
case LayerType.point:
return new PointTools(new PointToolsFactory());
}
}
}
How to avoid two switches no joining two methods in one switch? How to apply flexibility of OOP?
Class LayerManager manages a layers:
class LayerManager {
public selectedLayer: TreeNode;
public editableLayer: Layer;
public polygonsArea: string;
private readonly layers: TreeNode[];
constructor(private layerEditor: LayerEditor) {
this.layers = this.layerEditor.r.layersManager.editableLayers;
}
getLayers() {
return this.layers;
}
selectLayer(layerId: string) {
this.selectedLayer = { ...this.layerEditor.r.layersManager.getLayerConfig(layerId), enabled: true };
this.layerEditor.r.state.changeTreeNode(this.selectedLayer);
}
createLayerObject() {
this.editableLayer = FactoryLayer.create(this.selectedLayer.id, this.selectedLayer.type, this.layerEditor.r);
}
}
Command manager class makes works with commands. This class works as dispatcher and stores all commands.
class CommandManager {
public currentCommand: Command;
protected commands: Command[] = [];
protected undoCommand: Command;
constructor(public layerEditor: LayerEditor) {}
set(command: Command): void {
if (command === this.currentCommand) return;
this.currentCommand = command;
this.commands.push(this.currentCommand);
}
execute(): void {
this.currentCommand.execute();
}
undo(): void {
this.currentCommand.undo();
}
redo(): void {
this.currentCommand.redo();
}
cancel(): void {
while (this.commands.length) {
this.currentCommand = this.commands.pop();
this.undo();
}
}
complete(): void {
this.currentCommand.complete();
}
}
If you need anything else I will post
Essentially what you want to do is move all split decisions into one place. Instead of having many pairs of classes like PointTools, PointToolsFactory, SelectExistPointCommand, etc. you have one single PointConfig. You want your two configurations to match the same interface as much as possible. They both create a command and some tools. Other classes should be able to use the returned tools interchangeably without caring if it is PointTools or CommonTools. That way you can just call this.config.createTools().
This is the Factory Method Pattern and the C# Example should be readable as it's extremely similar to TypeScript. I've seen more of your code than what is posted here so I know that you were trying to do this already with a class FactoryLayer. But you want to define it so that the factory is the only switch that you need.
It should be something roughly along these lines:
interface LayerConfig {
id: string;
type: SemanticLayerType;
fields: Record<string, FieldConfig>;
createTools(): Button[];
createSelectCommand(map: ReonMap): Command;
}
class PointConfig implements LayerConfig {
readonly type = SemanticLayerType.Point;
readonly fields = {}; // fill this in
constructor(public readonly id: string) {}
createTools() {
return []; // fill this in
}
createSelectCommand(map: ReonMap): Command {
// fill this in
}
}
class PolygonConfig implements LayerConfig {
readonly type = SemanticLayerType.Polygon;
readonly fields = {}; // fill this in
constructor(public readonly id: string) {}
createTools() {
return []; // fill this in
}
createSelectCommand(map: ReonMap): Command {
// fill this in
}
}
class Layer {
tools: Button[];
constructor(public readonly config: LayerConfig) {
this.tools = config.createTools();
}
// other stuff
}
export class FactoryLayer {
static create(layerId: string, layerType: LayerType): Layer {
if (layerType === LayerType.common) {
return new Layer(new PolygonConfig(layerId));
}
if (layerType === LayerType.point) {
return new Layer(new PointConfig(layerId));
}
throw new Error("Unknown Layer Type " + layerType);
}
}
export class LayerEditor {
public layerManager: LayerManager;
constructor(private map: ReonMap, private commandManager: CommandManager) {
this.layerManager = new LayerManager(map); //this);
}
private getCurrentLayer(): Layer {
if (!this.layerManager.editableLayer) {
throw new Error("No Layer Selected");
}
return this.layerManager.editableLayer;
}
private executeCommand(): void {
// you could probably clean this up but you get the idea
this.commandManager.execute(
this.getCurrentLayer().config.createSelectCommand(this.map)
);
}
public getTools(): Button[] {
return this.getCurrentLayer().tools;
}
}
I have just tried to bring common things together. And trying to leverage Typescript features.
In both the private method you have this.layerManager.editableLayer.type common, now by adding an optional parameter and making use of returning multiple datatype. Below is the updated code.
If need you can default parameter as type = 'execute' and whenever needed pass parameter as create
private checkTool( type?: string): EditorTools | void {
const layerManagerType = this.layerManager.editableLayer.type
switch(layerManagerType) {
case LayerType.common:
return (type === 'execute') ?
this.commandManager.set(new SelectExistCommand(this.reonMap))
: new CommonTools(new CommonToolsFactory());
break;
case LayerType.point:
return ( type === 'execute') ?
this.commandManager.set(new SelectExistPointCommand(this.reonMap))
: new PointTools(new PointToolsFactory());
}
}

Is it possible to access class prototype's property?

I got a question but i don't know have the right words to ask it in just one phrase.
I'm writing my code in TypeScript and I want to achieve the following:
I got an abstract "Resource" class
// Resource.ts
abstract class Resource {
public abstract readonly type: string;
protected _id?: string;
get id() {
return this._id;
}
public static info() {
return {
type: this.prototype.type
};
}
}
and, let's say, a Post resource class that inherits from the abstract class
class PostResource extends Resource {
public readonly type = 'post'
}
I want to access the "type" property from the PostResource class prototype like I tried to do with the Resource.info() static method. Obviously, it returns undefined.
I also tried to instanciate the class inside the static method like that
public static info() {
return {
type: (new this()).type
}
}
but it throws an Error because I can't instanciate Resource as it is an abstract class.
I atempted to use static properties as well :
abstract class Resource {
public static readonly type: string;
public static info() {
return {
type: this.type
}
}
}
class PostResource extends Resource {
public static readonly type = 'post';
}
That theoricaly works, but then I loose all the benefits of inheritance because static propeties and methods cannot be abstract. For example, I could create a PostResource with no type static property and TypeScript would not warn me. type would then be undefined, which is not good because all Resource-extended class should have a type property.
So I'm looking for a solution to allow me to access all properties initialized in a Resource-extended class.
I hope my question is clear, thank you for your help !
EDIT:
I should give more details about my final goal.
I would like to have a list of classes that inherit Resource abstract class, and therefore have a "type" property. Ideally it should be a static property, cause I could simply iterate over the classes and just access the property.
But I also need this value inside the instances of this class, so for example i could have something like that:
console.log(PostResource.type); // > 'post'
const post1 = new PostResource();
console.log(post1.type); // > 'post';
What about having a property _type in the abstract class and initializing it in constructor of subclasses?
abstract class Resource {
protected _type: string;
constructor(type: string) {
this._type = type;
}
public info() {
return {
type: this._type
}
}
}
class PostResource extends Resource {
constructor() {
super('post');
}
}
class GetResource extends Resource {
constructor() {
super('get');
}
}
let postResource = new PostResource();
let getResource = new GetResource();
console.log(postResource.info()); // {type: 'post'}
console.log(getResource.info()); // {type: 'get'}
Edit
I'm not sure what you're trying to achieve, but here's an example for your updated requirements:
abstract class Resource {
}
class PostResource extends Resource {
static type = 'post';
public type = 'post';
}
let postResource = new PostResource();
console.log(PostResource.type); // post
const post1 = new PostResource();
console.log(post1.type); // post
Actually your attempt with instantiating the class inside the static method can work, you just need a proper typing for this:
public static info(this: new () => Resource) {
return {
type: new this().type
}
}
Playground
More info on function's this parameter annotation.

Workaround for accessing class type arguments in static method in Typescript

The following error
Static members cannot reference class type parameters.
results from the following piece of code
abstract class Resource<T> {
/* static methods */
public static list: T[] = [];
public async static fetch(): Promise<T[]> {
this.list = await service.get();
return this.list;
}
/* instance methods */
public save(): Promise<T> {
return service.post(this);
}
}
class Model extends Resource<Model> {
}
/* this is what I would like, but the because is not allowed because :
"Static members cannot reference class type parameters."
*/
const modelList = await Model.fetch() // inferred type would be Model[]
const availableInstances = Model.list // inferred type would be Model[]
const savedInstance = modelInstance.save() // inferred type would be Model
I think it is clear from this example what I'm trying to achieve. I want be able to call instance and static methods on my inheriting class and have the inheriting class itself as inferred type. I found the following workaround to get what I want:
interface Instantiable<T> {
new (...args: any[]): T;
}
interface ResourceType<T> extends Instantiable<T> {
list<U extends Resource>(this: ResourceType<U>): U[];
fetch<U extends Resource>(this: ResourceType<U>): Promise<U[]>;
}
const instanceLists: any = {} // some object that stores list with constructor.name as key
abstract class Resource {
/* static methods */
public static list<T extends Resource>(this: ResourceType<T>): T[] {
const constructorName = this.name;
return instanceLists[constructorName] // abusing any here, but it works :(
}
public async static fetch<T extends Resource>(this: ResourceType<T>): Promise<T[]> {
const result = await service.get()
store(result, instanceLists) // some fn that puts it in instanceLists
return result;
}
/* instance methods */
public save(): Promise<this> {
return service.post(this);
}
}
class Model extends Resource {
}
/* now inferred types are correct */
const modelList = await Model.fetch()
const availableInstances = Model.list
const savedInstance = modelInstance.save()
The problem that I have with this is that overriding static methods becomes really tedious. Doing the following:
class Model extends Resource {
public async static fetch(): Promise<Model[]> {
return super.fetch();
}
}
will result in an error because Model is no longer extending Resource correctly, because of the different signature. I can't think of a way to declare a fetch method without giving me errors, let alone having an intuitive easy way to overload.
The only work around I could get to work is the following:
class Model extends Resource {
public async static get(): Promise<Model[]> {
return super.fetch({ url: 'custom-url?query=params' }) as Promise<Model[]>;
}
}
In my opinion, this is not very nice.
Is there a way to override the fetch method without having to manually cast to Model and do tricks with generics?
You could do something like this:
function Resource<T>() {
abstract class Resource {
/* static methods */
public static list: T[] = [];
public static async fetch(): Promise<T[]> {
return null!;
}
/* instance methods */
public save(): Promise<T> {
return null!
}
}
return Resource;
}
In the above Resource is a generic function that returns a locally declared class. The returned class is not generic, so its static properties and methods have concrete types for T. You can extend it like this:
class Model extends Resource<Model>() {
// overloading should also work
public static async fetch(): Promise<Model[]> {
return super.fetch();
}
}
Everything has the types you expect:
Model.list; // Model[]
Model.fetch(); // Promise<Model[]>
new Model().save(); // Promise<Model>
So that might work for you.
The only caveats I can see right now:
There's a bit of duplication in class X extends Resource<X>() which is less than perfect, but I don't think you can get contextual typing to allow the second X to be inferred.
Locally-declared types tend not to be exportable or used as declarations, so you might need to be careful there or come up with workarounds (e.g., export some structurally-identical or structurally-close-enough type and declare that Resource is that type?).
Anyway hope that helps. Good luck!

listen to object property changes inside array in a service

So basically what I want to achieve is watching/listening objects changing inside of array within an injectable service using setters and getters to manipulate it's data
eg
#Injectable()
export class StorageService {
protected items: Array<any> = [];
constructor(private storage: Storage) {
this.storage.ready().then(() => {
StorageService.getGetters().forEach((get) => {
this.storage.get(get).then(res => this.items[get] = res);
});
});
}
public static getGetters(): string[] {
return Object.keys(this.prototype).filter(name => {
return typeof Object.getOwnPropertyDescriptor(this.prototype, name)["get"] === "function"
});
}
get Storage() {
return this.storage;
};
ClearStorage() {
this.storage.clear();
}
protected Setter(key: string, value: any): void {
this.items[key] = value;
this.storage.set(key, value);
}
protected Getter(key: string): any {
return this.items[key];
}
set User(value: User) {
this.Setter('User', value);
}
get User(): User {
return this.Getter('User');
}
}
where User interface is :
export interface User {
id: number;
role_id: number;
name: string;
email?: string;
}
now in any component or service/provider I can DI my StorageService so I can access the User getter.
so:
storage.User.name = 'testing';
now the name is changed , but I have no way to track that , so I can update my storage!
to update my storage I would do:
storage.User.name = 'testing';
storage.User = storage.User;
which is working , but I need a way to listen to any changes happens to the object properties, so I can update my storage...
I searched alot , and all I can find is watching components #Input() , which is not working in my case.
Hopefully I made my point clear.

how can I access typescript class public property at run time (debugging)? only the constructor and functions are accessible

I am implementing a DI container for my framework in typescript, and want to know my class constructor parameters and properties for instantiating. Here is an example:
interface IDriver
{
Drive(): void
}
class DriverA implements IDriver
{
public Tickets: Array<Ticket>;
public Name: String;
public Drive() {
//Driving...
}
}
I am passing the interface name IDriver as string (because I was not able to pass the interface as a parameter) and concrete class DriverA to my registration routine. Latter in resolving state, to instantiate DriverA, I got the constructor and the Drive method, but I can't find the properties such as Tickets and Name. How can I access those properties? is it possible?
Properties are only available if you initialize them e.g:
class DriverA
{
public Tickets = [];
public Name = "";
public Drive() {
//Driving...
}
}
will generate :
var DriverA = (function () {
function DriverA() {
this.Tickets = [];
this.Name = "";
}
DriverA.prototype.Drive = function () {
//Driving...
};
return DriverA;
})();
Notice this.Tickets. PS: they only get added after the constructor is called. i.e new DriverA()

Categories

Resources