TypeScript
TypeScript copied to clipboard
Polymorphic "this" for static members
When trying to implement a fairly basic, but polymorphic, active record style model system we run into issues with the type system not respecting this when used in conjunction with a constructor or template/generic.
I've posted before about this here, #5493, and #5492 appears to mention this behavior also.
And here is an SO post this that I made: http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved
I have recycled my example from #5493 into this ticket for further discussion. I wanted an open ticket representing the desire for such a thing and for discussion but the other two are closed.
Here is an example that outlines a model Factory which produces models. If you want to customize the BaseModel that comes back from the Factory you should be able to override it. However this fails because this cannot be used in a static member.
// Typically in a library
export interface InstanceConstructor<T extends BaseModel> {
new(fac: Factory<T>): T;
}
export class Factory<T extends BaseModel> {
constructor(private cls: InstanceConstructor<T>) {}
get() {
return new this.cls(this);
}
}
export class BaseModel {
// NOTE: Does not work....
constructor(private fac: Factory<this>) {}
refresh() {
// get returns a new instance, but it should be of
// type Model, not BaseModel.
return this.fac.get();
}
}
// Application Code
export class Model extends BaseModel {
do() {
return true;
}
}
// Kinda sucks that Factory cannot infer the "Model" type
let f = new Factory<Model>(Model);
let a = f.get();
// b is inferred as any here.
let b = a.refresh();
Maybe this issue is silly and there is an easy workaround. I welcome comments regarding how such a pattern can be achieved.
The size and shape of my boat is quite similar! Ahoy!
A factory method in a superclass returns new instances of its subclasses. The functionality of my code works but requires me to cast the return type:
class Parent {
public static deserialize(data: Object): any { ... create new instance ... }
// Can't return a this type from statics! ^^^ :(
}
class Child extends Parent { ... }
let data = { ... };
let aChild: Child = Child.deserialize(data);
// ^^^ Requires a cast as type cannot be inferred.
I ran into this issue today too!
A fixup solution is to pass the child type in as a generic extending the base class, which is a solution I apply for the time being:
class Parent {
static create<T extends Parent>(): T {
let t = new this();
return <T>t;
}
}
class Child extends Parent {
field: string;
}
let b = Child.create<Child>();
Is there a reason this issue was closed?
The fact that polymorphic this doesn't work on statics basically makes this feature DOA, in my opinion. I've to date never actually needed polymorphic this on instance members, yet I've needed every few weeks on statics, since the system of handling statics was finalized way back in the early days. I was overjoyed when this feature was announced, then subsequently let down when realizing it only works on instance members.
The use case is very basic and extremely common. Consider a simple factory method:
class Animal
{
static create(): this
{
return new this();
}
}
class Bunny extends Animal
{
hop()
{
}
}
Bunny.create().hop() // Type error!! Come on!!
At this point I've been either resorting to ugly casting or littering static create() methods in each inheritor. Not having this feature seems like a fairly large completeness hole in the language.
@paul-go the issue is not closed... ?
@paul-go I've been frustrated with this issue also but the below is the most reliable workaround i've found. Each Animal subclass would need to call super.create() and just cast the result to it's type. Not a big deal and it's a one liner that can easily be removed with this is added.
The compiler, intellisense, and most importantly the bunny are all happy.
class Animal {
public static create<T extends Animal>(): T {
let TClass = this.constructor.prototype;
return <T>( new TClass() );
}
}
class Bunny extends Animal {
public static create(): Bunny {
return <Bunny>super.create();
}
public hop(): void {
console.log(" Hoppp!! :) ");
}
}
Bunny.create().hop();
\\
\\_ " See? I am now a happy Bunny! "
(') " Don't be so hostile! "
/ )= " :P "
o( )_
@RyanCavanaugh Oops ... for some reason I confused this with #5862 ... sorry for the battle axe aggression :-)
@Think7 Yep ... hence the "resorting to ugly casting or littering static create() methods in each inheritor". It's pretty hard though when you're a library developer and you can't really force end users to implement a bunch of typed static methods in the classes that they inherit from you.
lawl. Totally missed everything under your code :D
Meh was worth it, Got to draw a bunny.
:+1: bunny
:rabbit: :heart:
+1, would definitely like to see this
Have there been any discussion updates on this topic?
It remains on our enormous suggestion backlog.
Javascript already acts correctly in such a pattern. If TS could follow also that would save us from a lot of boilerplate/extra code. The "model pattern" is a pretty standard one, I'd expect TS to work as JS does on this.
I would also really like this feature for the same "CRUD Model" reasons as everyone else. I need it on static methods more than instance methods.
This would provide a neat solution to the problem described in #8164.
It's good that there're "solutions" with overrides and generics, but they aren't really solving anything here – the whole purpose of having this feature is to avoid such overrides / casting and create consistency with how this return type is handled in instance methods.
I'm working on the typings for Sequelize 4.0 and it uses an approach where you subclass a Model class. That Model class has countless static methods like findById() etc. that of course do not return a Model but your subclass, aka this in the static context:
abstract class Model {
public static tableName: string;
public static findById(id: number): this { // error: a this type is only available in a non-static member of a class or interface
const rows = db.query(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
const instance = new this();
for (const column of Object.keys(rows[0])) {
instance[column] = rows[0][column];
}
return instance;
}
}
class User extends Model {
public static tableName = 'users';
public username: string;
}
const user = User.findById(1); // user instanceof User
This is not possible to type currently. Sequelize is the ORM for Node and it is sad that it cannot be typed. Really need this feature. The only way is to cast everytime you call one of these functions or to override every single one of them, adapt the return type and do nothing but call super.method().
Also kind of related is that static members cannot reference generic type arguments - some of the methods take an object literal of attributes for the model that could be typed through a type argument, but they are only available to instance members.
😰 Can't believe this still isn't fixed / added.....
We could make a good use of this:
declare class NSObject {
init(): this;
static alloc(): this;
}
declare class UIButton extends NSObject {
}
let btn: UIButton = UIButton.alloc().init();
here is a use case that i wish worked (migrated from https://github.com/Microsoft/TypeScript/issues/9775 which i closed in favor of this)
Currently the parameters of a constructor cannot use this type in them:
class C<T> {
constructor(
public transformParam: (self: this) => T // not works
){
}
public transformMethod(self: this) : T { // works
return undefined;
}
}
Expected: this to be available for constructor parameters.
A problem that is sought to be solved:
- be able to base my fluent API either around itself of around another fluent API reusing the same code:
class TheirFluentApi {
totallyUnrelated(): TheirFluentApi {
return this;
}
}
class MyFluentApi<FluentApi> {
constructor(
public toNextApi: (self: this) => FluentApi // let's imagine it works
){
}
one(): FluentApi {
return this.toNextApi(this);
}
another(): FluentApi {
return this.toNextApi(this);
}
}
// self based fluent API;
const selfBased = new MyFluentApi(this => this);
selfBased.one().another();
// foreign based fluent API:
const foreignBased = new MyFluentApi(this => new TheirFluentApi());
foreignBased.one().totallyUnrelated();
Future-proof workaround:
class Foo {
foo() { }
static create<T extends Foo>(): Foo & T {
return new this() as Foo & T;
}
}
class Bar extends Foo {
bar() {}
}
class Baz extends Bar {
baz() {}
}
Baz.create<Baz>().foo()
Baz.create<Baz>().bar()
Baz.create<Baz>().baz()
This way, when TypeScript supported this for static members, new user code will be Baz.create() instead of Baz.create<Baz>() while old user code will be work just fine. :smile:
This is really needed! especially for DAO that have static methods returning the instance. Most of them defined on a base DAO, like (save, get, update etc...)
I can say that it might cause some confusion, having this type on a static method resolve to the class it's in and not the type of the class (i.e: typeof).
In real JS, calling a static method on a type will result in this inside the function being the class and not an instance of it... so it inconsistent.
However, in people intuition I think the first thing that pops up when seeing this return type on a static method is the instance type...
@shlomiassaf It would not be inconsistent. When you specify a class as a return type for a function like User, the return type will be an instance of the user. Exactly the same, when you define a return type of this on a static method the return type will be an instance of this (an instance of the class). A method that returns the class itself can then be modeled with typeof this.
@felixfbecker this is totally a view point thing, this is how you choose to look at it.
Let's inspect what happens in JS, so we can infer logic:
class Example {
myFunc(): this {
return this;
}
static myFuncStatic(): this {
return this; // this === Example
}
}
new Example().myFunc() // instanceof Exapmle === true
Example.myFuncStatic() // === Example
Now, this in real runtime is the bounded context of the function, this is exactly what happens in fluent api like interfaces and the polymorphic this feature help by returning the right type, which is just aligning with how JS works, A base class returning this returns the instance which created by the derived class. The feature is actually a fix.
To sum up:
A method returning this that is defined as part of the prototype (instance) is expected to return the instance.
Continuing with that logic, a method returning this that is defined as part of the class type (prototype) is expected to return the bounded context, which is the class type.
Again, no bias, no opinion, simple facts.
Intuition wise, I will feel comfortable having this returned from a static function represent the instance since it's defined within the type, but thats me. Others might think differently and we can't tell them their wrong.
The problem is that it needs to be possible to type both a static method that returns a class instance and a static method that returns the class itself (typeof this). Your logic makes sense from a JS perspective, but we are talking about return types here, and using a class as a return type (here this) in TypeScript and any other language always means the instance of the class. To return the actual class, we have the typeof operator.
@felixfbecker This raises another issue!
If the type this is the instance type, it's different than what the this keyword refers to in the body of the static method. return this yields a function return type of typeof this, which is totally weird!
No, its not. When you define a method like
getUser(): User {
...
}
you expect to get a User instance back, not the User class (that's what typeof is for). It's how it works in every typed language. Type semantics are simply different from runtime semantics.
Why not use this or static keywords as a constructor in func for manipulating with a child class?
class Model {
static find():this[] {
return [new this("prop")]; // or new static(...)
}
}
class Entity extends Model {
constructor(public prop:string) {}
}
Entity.find().map(x => console.log(x.prop));
And if we compare this with an example in JS, we'll see what it works correctly:
class Model {
static find() {
return [new this]
}
}
class Entity extends Model {
constructor(prop) {
this.prop = prop;
}
}
Entity.find().map(x => console.log(x.prop))
You cannot use the this type on a static method, I think that's the entire root of the issue.
@felixfbecker
Consider this:
class Greeter {
static getHandle(): this {
return this;
}
}
This type annotation is intuitive, but incorrect if the type this in a static method is the class instance. The keyword this has a type of typeof this in a static method!
I do feel that this should refer to the instance type, not the class type, because we can get a reference to the class type from the instance type (typeof X) but not vice-versa (instancetypeof X?)