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.
Related
I'm trying to implement mixins in TypeScript.
Here's the code:
type Mixable = new (...args: any[]) => {};
class Base {
constructor(protected _a: string, protected _b: string) {}
}
const A = (Base: Mixable) => class A extends Base {
a() {
console.log(this._a);
return this;
}
};
const B = (Base: Mixable) => class B extends Base {
b() {
console.log(this._b);
return this;
}
};
const C = A(B(Base));
const o = new C("a","b");
o.a().b();
But unfortunately the compiler is failed to identify the properties shared among the mixins, chain methods and emitted errors.
Here's the playground link.
P.S. If you run this, it runs successfully, and generates the expected output without any JS error.
In order to access properties _a and _b you need to enforce that the class which you are applying the mixin to has these properties.
Also mixins can be tricky in typescript with protected properties, so I made them public.
interface HasAB {
_a: string;
_b: string;
}
type Mixable = new (...args: any[]) => HasAB
class Base {
constructor(public _a: string, public _b: string) {}
}
Typescript Playground Link
This clears up your issue with accessing properties.
Edit:
I've been playing with this more and unfortunately mixins just to do not work well with non-public properties. With this setup the composed classes are able to access the protected properties of the passed-in class, but they give us error TS4094 "Property '_a' of exported class expression may not be private or protected." because properties of a mixin must be public.
I'm not confident enough to say it's straight-up impossible, but if I were you I would consider a different design pattern. You can add accessor methods getA() and getB() to your class Base. Or you can define readonly public properties a and b which access the underlying private properties _a and _b.
Here's how that second case breaks down:
We define an interface AB which has public properties a and b. Recall that all properties in a typescript interface are inherently public.
interface AB {
a: string;
b: string;
}
We declare that something is considered Mixable if is can be created with the keyword new and if that created object has a and b.
type Mixable = new (...args: any[]) => AB;
We alter Base such that it implements interface AB, which will make it Mixable. You can explicitly write class Base implements AB but you don't have to. It will be assignable to Mixable as long as it can read properties a and b. We use javascript getters to implement a and b as readonly properties which will read the private value of _a and _b but cannot modify them.
class Base {
constructor( protected _a: string, protected _b: string) {}
get a(): string {
return this._a;
}
get b(): string {
return this._b;
}
}
Now for our mixins. We say that the class to be mixed must be of type Mixable. We use the generic <T extends Mixable>(Mixed: T) so that we know that the returned class will have all of the properties and methods of the passed-in class, not just those in Mixable. This fixes the issues with chaining.
Note that I renamed the methods in your example because I used a and b as the names of the properties so I can't also use them as the name of the methods.
const A = <T extends Mixable>(Mixed: T) => class A extends Mixed {
aMethod() {
console.log(this.a);
return this;
}
};
const B = <T extends Mixable>(Mixed: T) => class B extends Mixed {
bMethod() {
console.log(this.b);
return this;
}
};
Now you no longer have issues with any of this:
const C = A(B(Base)); // fine because Base extends Mixable
const o = new C("a","b");
o.aMethod().bMethod(); // fine because o.aMethod() returns an object with all of the abilities of C.
Typescript Playground Link
Thanks, guys for your contributions. It added a huge value to the question.
Before I go further, let me explain my original intention and the misunderstanding raised from my code.
My primary goal to use mixins is to decompose my large classes. As because this will be applied in different classes, I am creating a helper module. In order to keep the example understandable, I posted a simpler implementation.
Regarding the Base identifier, I did not mean the Base argument is referring to the Base class, but the Parent (i.e. the base of the extension). Anyways, to get rid of the confusion I'll be using the identifier Parent instead.
So, here's what I've come up with which I believe is pretty much extensible. Please ignore the weird name Libbable (this is just to demonstrate the idea).
type Mixable<T = {}> = new (...args: any[]) => T;
interface Libbable {
x: string;
a(): any;
}
type Mixin = Mixable<Libbable>;
class Lib {
constructor(public x: string) {}
a(): this { return this }
}
const A = <T extends Mixin>(Parent: T) => class extends Parent {
a() {
console.log(this.x);
return this;
}
};
const B = <T extends Mixin>(Parent: T) => class extends Parent {
b() {
return this.a();
}
};
const L = A(B(Lib));
const o = new L("x");
o.a().b();
Link to the Playground
Because of the design issue, currently, TypeScript is not able to share non-public properties among mixins. I really look forward to being able to use this, otherwise, the powerful access modifiers are simply useless here.
As it is necessary to declare the methods in the Base class to use them in the mixins, they will be gets called once the applicable class methods of the mixins are called. This is not what I want to happen. JS allows us to get rid of this, but TS doesn't. If you have a better idea to achieve what I want, feel free to share.
Use case: I want to model a generic system for look-up tables (a Table class) of instances of a specific class (a Model class).
A minimal example of what I would like to do:
// Generic part
abstract class Table<T extends Model> {
instances: Map<number, T> = new Map();
}
abstract class Model {
constructor(
public readonly id: number,
public table: Table<this> // Error
) {
table.instances.set(id, this);
}
}
// Example: a table of Person objects
class Person extends Model {
constructor(
id: number,
table: Table<this>, // Error
public name: string
) {
super(id, table);
}
}
class PersonTable extends Table<Person> {}
const personTable = new PersonTable();
const person = new Person(0, personTable, 'John Doe');
// Note: the idea of using `this` as generic type argument is to guarantee
// that other models cannot be added to a table of persons, e.g. this would fail:
// class SomeModel extends Model { prop = 0; }
// const someModel = new SomeModel(1, person.table);
// ^^^^^^^^^^^^
Unfortunately, TypeScript complains about the this type in the constructor. Why isn't this allowed? Is there a better way to do this?
Unsafe alternative
For now I'm using the following unsafe alternative.
// Generic part
abstract class Table<T extends Model> {
instances: Map<number, T> = new Map();
}
abstract class Model {
public table: Table<this>;
constructor(
public readonly id: number,
table: Table<Model>
) {
table.instances.set(id, this);
this.table = table as Table<this>;
}
}
// Example: a table of Person objects
class Person extends Model {
constructor(
id: number,
table: Table<Person>,
public name: string
) {
super(id, table);
}
}
class PersonTable extends Table<Person> {}
Answer to comment
To answer a comment of Liam: a very simple safe example of the this type.
class A {
someInstances: this[] = [];
}
class B extends A {
someProp = 0;
}
const a = new A();
const b = new B();
a.someInstances.push(b);
// This isn't allowed: b.someInstances.push(a);
I think I was able to propose a solution to your problem. Unfortunately due to the language restrictions, it may not be very elegant, but it is not a bad one either.
Unfortunately, the keyword "this" can not be used as a type so it can not be used in generics, as other answers stated. In your case, you can rewrite your code, and instead of "this", just use the current type, BUT, this will not be the "guarantee" that objects inside your Table will be of the same type, which is what you described as necessary.
Unfortunately, in JavaScript/TypeScript, you can not guarantee that objects in any generic collection are of the same type "by typings", because TypeScript does not provide tools such as covariance, contravariance, and invariance. You have to ensure it using code and checks. This is a known issue for example in promises, where you can return types that you should not. (At leas this is what I know and found just now, not 100 % sure)
To create an invariant table, where all members are of the same type, we have to check every inputted element. I proposed one possible model, where every table accepts a user-defined function that checks what types can be let it and what types are forbidden:
interface TypeGuard<T>
{
(inputObject: T): boolean;
}
// Generic part
class SingleTypeTable<T>
{
private typeGuard: TypeGuard<T>;
constructor(typeGuard: TypeGuard<T>)
{
this.typeGuard = typeGuard;
}
Add(item: T)
{
//Check the type
if (!this.typeGuard(item))
throw new Error("I do not like this type");
//...
}
}
The person guard works as follows:
const personGuard: TypeGuard<Person> = function (person: Person): boolean
{
return person instanceof Person;
}
personGuard(new Person(...)); //true
personGuard("string" as any as Person); //false
Now you can create your models and persons as follows:
// Some abstract model
abstract class Model<T>
{
constructor(
public readonly id: number,
public table: SingleTypeTable<T> //Table of models
)
{
}
}
// Example: a table of Person objects
class Person extends Model<Person>
{
constructor(
id: number,
table: SingleTypeTable<Person>,
public name: string
)
{
super(id, table);
}
}
//Usage
const personTable = new Table<Person>(personGuard);
const person = new Person(0, personTable , 'John Doe');
I understand that I might have changed your model structure a little bit but I do not know your overall picture and I am sure that if you like this solution, you can change it to your likings, this is just a prototype.
I hope this is what you need.
This part of my answer tries to explain my theory of why you can not use "this" keyword in a constructor as a parameter type.
First of all, you cant use "this" as a type in a function. You can not do this:
function foo(a: this) {} //What is "this"? -> Error
Before I explain further, we need to take a trip back to plain JavaScript. One of the ways to create an object is to "instantiate a function", like this:
function Animal(name) { this.name = name; }
var dog = new Animal("doggo");
This is almost exactly what TypeScript classes are compiled to. You see, this Animal object is a function, but also a constructor for the Animal object.
So, why you can not use "this" keyword in the TypeScript constructor? If you look above, a constructor is compiled into a function, and constructor parameters are just some parameters of a function, and these can not have the type of "this".
However, the TypeScript compiler might be able to figure out the "this" type even is the constructor parameter. This is certainly a good suggestion for a feature for the TypeScript team.
The Type<ContentType> annotations is used for types only, means Table<this> is not valid, because this is always refering to an instance and not a class/type/interface etc. Means: it is valid to pass this as an argument in table.instances.set(id, this);, but Table<this> is not, you should change this to Table<Model>.
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();
I'm trying to figure out how to use type-safety provided by TypeScript with old plain constructor functions in JS. I have a very simple example, that looks straightforward, but I miss something and can't make it compile with TypeScript:
interface IMyService {
new(): IMyService //I'm not sure if this line should be here, I just trying to make it working...
doSomething(name: string): void
}
function MyService(this: IMyService): void {
let _name = ""
this.doSomething = (name) => {
_name = name
}
}
//The line below won't compile and it saying:
//"new" expression, whose target lacks a construct signature, implicitly has an "any" type
let service = new MyService();
service.setName("Test Name")
What I'm missing? I know the preferred way of using TypeScript is with "class", but in my case I would like to use simple constructor functions.
You cant really type a function declaration (or at least i dont know how). However you can type a variable, and assign a function to it. And then we can define a constructor type:
interface IMyService {
doSomething(name: string): void;
}
interface IMyServiceConstructor {
new(): IMyService;
}
const MyService: IMyServiceConstructor = function(this: IMyService){
//...
};
That can be shortified through using an inline type:
const MyService: { new(): IMyService } = function(){
//...
};
What's stopping you from doing this:
class MyService {
// declare instance method
doSomething: (x: string) => void;
// this is really your function
constructor() {
let _name = "";
this.doSomething = (name) => {
_name = name;
}
}
}
let service = new MyService();
service.doSomething("Test Name");
This emits almost the same code as your original. It's still using a variable local to the constructor function scope, and an instance method instead of a class method. (Instance methods are generally frowned upon because you're creating closures for each instance but that's up to you.)
And TypeScript understands that MyService is newable and all the other goodness you want. Jumping through hoops with constructor type signatures and convincing TypeScript that your function is the right type doesn't seem worth it to me.
Hope that helps.
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.