I'm trying to wrap class constructor and inject to some logic by using class decorator. Everything worked fine until I have tried to extend wrapped class: Extended class don't have methods in prototype.
function logClass(Class) {
// save a reference to the original constructor
const _class = Class;
// proxy constructor
const proxy = function(...args) {
const obj = new _class(...args);
// ... add logic here
return obj
}
// copy prototype so intanceof operator still works
proxy.prototype = _class.prototype;
// return proxy constructor (will override original)
return proxy;
}
#logClass
class Base {
prop = 5;
test() {
console.log("test")
}
}
class Extended extends Base {
test2() {
console.log("test2")
}
}
var base = new Base()
base.test()
var ext = new Extended()
console.log(ext.prop)
ext.test()
ext.test2() // TypeError: ext.test2 is not a function
Okay so I tried to figure out what is "wrong" with your code, but I was not able to make it work because it didn't typecheck. So, as a last resort, I'm posting a partial answer of my attempt, which works (with some quirks) so I can help other users who are more savvy with TypeScript.
First of all, the quirks: class decorators in TS cannot modify the structure of a type, so if you wanted to, for example, add a method to the decorated class, you would be able to do it but you would have to eat up/suppress unavoidable type errors (TS2339) when calling those methods.
There is a work around for this in this other question: Typescript adding methods with decorator type does not exist, but you would lose this current clean syntax for decorators if you do this.
Now, my solution, taken more or less directly from the documentation:
function logClass<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(args);
// ...add programmatic logic here
// (`super` is the decorated class, of type `T`, here)
}
// ...add properties and methods here
log(message: string) { // EXAMPLE
console.log(`${super.constructor.name} says: ${message}`);
}
}
}
#logClass
class Base {
prop = 5;
test() {
console.log("test");
}
constructor() {}
}
class Extended extends Base {
test2() {
console.log("test2");
}
}
var base = new Base();
base.test();
var ext = new Extended();
console.log(ext.prop);
//base.log("Hello"); // unavoidable type error TS2339
ext.test();
ext.test2();
Related
I got a problem with calling super inside of the child static method.
const _todos = [];
class Todo {
constructor(title) {
this.title = title;
this.id = Math.round(Math.random() * 100);
_todos.push({title, id: this.id});
console.log(`Todo ID: ${this.id}. DONT FORGET IT!`);
}
static TodoerVersion = '1.8';
static removeTodo(id) {
const todoIndex = _todos.findIndex(t => t.id == id);
_todos.splice(todoIndex, 1);
}
}
class TodoV2 extends Todo {
static addTodo(title) {
super(title);
}
static addDescription(todo, description) {
todo.description = description;
}
static TodoerVersion = '2.0';
};
new TodoV2("play guitar");
Why does it not work?
But if i call super in normal method, it would works just fine.
super is only callable within the constructor function of a class.
Two answers for you:
The question you actually asked.
What I think you should do instead. :-)
Answering the question you asked:
JavaScript is very special in this regard: this has meaning in static methods (as long as you call them correctly, e.g. TodoV2.addTodo("title")): it's the constructor function you called the static method on. And a subclass constructor function inherits from its superclass constructor function (yes, really). So you can access the constructor function for the parent class using Object.getPrototypeOf(this) in the static method:
// This is something unusual that JavaScript actually does support
static addTodo(title) {
const ctor = Object.getPrototypeOf(this);
return new ctor(title);
}
To handle the case where the user may have called addTodo in a way that doesn't set this, you might do something like this to default to TodoV2 if this is undefined:
static addTodo(title) {
const ctor = Object.getPrototypeOf(this ?? TodoV2);
return new ctor(title);
}
What I think you should do instead
You shouldn't be using a static method for this. Instead, define a constructor for TodoV2:
class TodoV2 extends Todo {
constructor(title, description) {
super(title);
this.description = description ?? "";
}
static addTodo(title) { // Why no `description`?
return new this(title); // Or `new TodoV2(title)`
}
}
You might also look into the Symbol.species pattern if you want subclasses to create instances of superclasses.
In python there's something like __call__ for this. Consider the following example:
class MyClass {
__call__() { return 'called!' }
}
const myType = new MyClass();
myType(); // called!
The question is what should I replace __call__ with?
I was doing some research, and most of the answers recommend __proto__, but it doesn't seem to work.
It is not possible out-of-the-box, but you can extend Function, and use the Function constructor to forward a call to __call__. If you have multiple classes that need this feature, extend Function only once into -- let's say -- a Callable class, and then inherit your other classes from that:
class Callable extends Function {
constructor() {
super("...args", "return this.__call__(...args)");
return this.bind(this);
}
}
class Class extends Callable {
__call__() { return 'called!' }
}
let inst = new Class();
console.log(inst());
Background
In JavaScript an object is callable when, and only if, it has the [[Call]] internal slot. But there is (currently) no way to give any given object this slot via JavaScript code. One must start with a function object and extend that.
Adding a constructor, inheritance
The above solution allows the constructor to define properties in the usual way: the constructed object is an instance of the class:
class Callable extends Function {
constructor() {
super("...args", "return this.__call__(...args)");
return this.bind(this);
}
}
class Class extends Callable {
constructor(data) {
super();
this.x = data;
}
__call__() { return 'called!' }
}
let inst = new Class(42);
console.log(inst instanceof Class); // true
console.log(inst.x); // 42
console.log(inst());
You can use constructor.
class Example {
constructor() {
// gets called on class initialization
}
}
Inside the constructor you can also call other methods if you want.
However this won't create an invoke function like using PHP's __invoke if that's what you meant. If that's what you're looking for then I don't know.
I am looking for a way to call new this from within a class method.
class Example {
fork() {
return new this();
}
}
const x = new Example().fork(); // instance of example
class Alpha extends Example {}
const x = new Alpha().fork(); // expected instance of Alpha but is example
I assume you mean you want to call new this.constructor() (see MDN doc) and not new this(), since it is vanishingly rare that the instance of a class will also be a class constructor. At runtime this is all you need.
Unfortunately the typing for constructor in TypeScript is messy. See microsoft/TypeScript#3841 for details, but the main issue is that since subclass constructors can require a different set of arguments from their superclass constructors, if this.constructor were strongly typed, then many class hierarchies would fail to form a valid subtype hierarchy and would therefore violate substitutatibility.
So in TypeScript, constructor is typed only as Function, and therefore new this.constructor() will yield an error:
return new this.constructor(); // error!
// This expression is not constructable.
In order to tell the compiler that this.constructor is a zero-arg constructor function, you'll have to either use a type assertion like this:
class Example {
fork(): this {
return new (this.constructor as new () => this)()
}
}
or add a strongly-typed constructor property declaration to your class like this:
class Example2 {
['constructor']: new () => this
fork(): this {
return new this.constructor()
}
}
Both solutions work for your presented example use case:
const x = new Example().fork(); // Example
class Alpha extends Example { }
const y = new Alpha().fork(); // Alpha
But neither solution will prevent you from calling fork() on an instance of a subclass whose constructor requires a parameter
class Blop extends Alpha {
constructor(x: string) {
super();
console.log(x.toUpperCase());
}
}
const z = new Blop("oops").fork(); // error at runtime!!
Without a good solution to microsoft/TypeScript#3841, I think this might be the best you can get.
Playground link to code
First of all I'm a newbie in Typescript so title may be inaccurate because I don't know how that code works properly.
I'm using the Klasa framework which is a Discord bot framework made top of Discord.js. They recently added plugin functionality and there is lots of examples written in regular ES6...
const { Client, util: { mergeDefault } } = require('klasa');
const DriverStore = require('../lib/structures/DriverStore');
const { OPTIONS } = require('../lib/util/constants');
class MusicClient extends Client {
constructor(config) {
super(config);
this.constructor[Client.plugin].call(this);
}
static [Client.plugin]() {
mergeDefault(OPTIONS, this.options);
this.drivers = new DriverStore(this);
this.registerStore(this.drivers);
}
}
module.exports = MusicClient;
Type of Client.plugin is Symbol. How this code work? And how can I achieve something similar to this with TypeScript or it is doable?
I tried doing it like this:
import { KlasaClientOptions, Client } from "klasa"
export class ExtendedClient extends Client {
public prop: string;
constructor(options: KlasaClientOptions) {
super(options);
// Element implicitly has an 'any' type 'typeof KlasaClient' has no index signature.
this.constructor[Client.plugin].call(this);
// Also trying to use ExtendedClient.plugin.call() gives me
// Property 'call' does not exist on type 'symbol'
}
static [Client.plugin]() {
// Property 'prop' does not exist of type 'typeof ExtendedClient'
this.prop = "somestring";
}
}
Edit: I fixed the error after I found out static [Client.plugin]() has the context of KlasaClient so I changed it as
import { KlasaClientOptions, Client } from "klasa"
export class ExtendedClient extends Client {
public prop: string;
constructor(options: KlasaClientOptions) {
super(options);
(this.constructor as any)[Client.plugin].call(this);
}
static [Client.plugin](this: ExtendedClient) {
this.prop = "somestring";
}
}
and the problems are solved...
The code is fairly odd, and unless there's a strong design constraint forcing it to be that way, it's not best practice.
It's defining a static method, but calling it as though it were an instance method. Which is part of why TypeScript is having issues with it.
The key parts are:
This:
static [Client.plugin]() {
this.registerStore(this.drivers);
}
...which defines a static method (a method on the constructor function) with the name from Client.plugin (which you've said is a Symbol, but it doesn't really matter whether it's a Symbol or a string).
And this in the constructor:
this.constructor[Client.plugin].call(this);
...which is what's calling it as though it were an instance method. this.constructor accesses the constructor function that created this (loosely speaking, that's not entirely accurate), and so this.constructor[Client.plugin] accesses the static method with the name from Client.plugin. Then .call(this) calls that method, but sets this as this during the call, a though it were an instance method. So within the call to Client.plugin, this is an instance of the class (whereas normally this would be the constructor function).
Which is very odd.
In terms of making TypeScript okay with it, I think I'd probably approach it like this:
import { KlasaClientOptions, Client } from "klasa";
export class ExtendedClient extends Client {
public prop: string;
constructor(options: KlasaClientOptions) {
super(options);
(this.constructor as any)[Client.plugin].call(this);
}
static [Client.plugin]() {
const inst = this as ExtendedClient;
// Now use `inst` instead of `this` where your example code uses `this`, so:
inst.prop = "somestring";
}
}
The key bits are:
This:
(this.constructor as any)[Client.plugin].call(this);
...by using any we defeat TypeScript's type checking, but we basically have to for this odd structure.
And this:
const inst = this as ExtendedClient;
...by using as ExtendedClient we're telling TypeScript that although normally it would expect this to be the constructor function, it's actually an ExtendedClient. We do that once with a constant to avoid having to repeat it everywhere.
I was previously rolling my own Javascript OOP but now I'm playing with ES6 and want to use the class defined after definition in a generic way.
Note
Any answer with new in it is not what I'm after.
Pseudo code:
// base.js
class Base {
constructor(arg) {
this.arg = arg;
}
// This is the behaviour I'm after
//afterDefined(cls) {
afterExtended(cls) { // probably a better name
console.log(`Class name ${cls.prototype.name}`);
}
}
// frombase.js
class FromBase extends Base {
constructor({ p1='val1', p2='val2'} = {}) {
super(...arguments);
this.p1 = p1;
this.p2 = p2;
}
}
The output in the console should be:
'Class name FromBase'
So far the only solution I have come up with is to have a static method on Base and call it after the class declaration when I define a new class but I will most likely forget to do this more than once.
Just to be really thorough on why I don't like the static solution; it will force me to import Base in every single file.
Example using a static method (which I don't want) https://jsfiddle.net/nL4atqvm/:
// base.js
class Base {
constructor(arg) {
super(...arguments);
this.arg = arg;
}
// This is the behaviour I'm after
static afterExtended(cls) {
console.log(`Class name ${cls.name}`);
}
}
// frombase.js
class FromBase extends Base {
}
// just after defining the FromBase class
Base.afterExtended(FromBase);
There is no javascript built-in trigger that is calling a method on a class when a subclass is defined that extends from it.
Because you're rolling your own library, you could craft some kind of method the creates and returns a new class that extends a given base class. Maybe check out this answer that may help how to define your classes: Instantiate a JavaScript Object Using a String to Define the Class Name
You could also check how other javascript libraries creates (sub)classes. For example, Ext JS has a ClassManager that you could look into.
http://docs.sencha.com/extjs/6.5.1/classic/Ext.ClassManager.html (docs)
http://docs.sencha.com/extjs/6.5.1/classic/src/ClassManager.js.html (source)
When this question would be about instantiation and not about defining classes, I would say:
afterDefined(cls) {
console.log(`Class name ${this.constructor.name}`);
}
Usage:
let x = new FromBase()
x.afterDefined() // --> Class name FromBase
To get the name of the class, use
static afterDefined(cls) {
console.log(`Class name ${this.name}`);
}
Is this what you're looking for?
class Base {
constructor(arg) { this.arg = arg; }
static afterDefined(cls) {
console.log(`Class name ${this.constructor.name}`);
}
}
Base = new Proxy(Base, {
get: function(target, key, receiver) {
if (typeof target == 'function' && key == 'prototype' && target.name == Base.name) {
Reflect.apply(Base.afterDefined, Reflect.construct(class FromBase {}, []), [])
}
return target[key];
}
});
class FromBase extends Base {}
In order for this to load class names, you will have to embed or forward-declare that information inside of the Proxy receiver prior to extending from it (which means you either need to enumerate ahead of time which classes you'll be inheriting to or to have some function call just prior to that class's declaration).
There are a bunch of other neat total hacks in the same vein such as looking at the source (text) of the file that you just loaded JavaScript from and then parsing that text as if it were JavaScript (people have used this to write Scheme interpreters that can accept new code inside of <script> tags).
If you are a library author intending to target Node, there are even more ways to go about doing this.
// base.js
class Base {
constructor(arg) {
this.arg = arg;
}
// This is the behaviour I'm after
afterDefined(cls) {
console.log(`Class name ${cls}`);
}
}
// frombase.js
class FromBase extends Base {
constructor(arg) {
super(arg)
}
}
let f = new FromBase();
f.afterDefined('text');//this you text or object
have to be aware of is. file loading order, super is an instance of the parent class. good luck.