TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Polymorphic "this" for static members

Open xealot opened this issue 10 years ago • 196 comments

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.

xealot avatar Dec 01 '15 20:12 xealot

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.

Think7 avatar Dec 17 '15 09:12 Think7

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

LPGhatguy avatar Jan 06 '16 00:01 LPGhatguy

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 avatar Feb 08 '16 22:02 paul-go

@paul-go the issue is not closed... ?

RyanCavanaugh avatar Feb 08 '16 22:02 RyanCavanaugh

@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( )_


Think7 avatar Feb 08 '16 22:02 Think7

@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.

paul-go avatar Feb 08 '16 23:02 paul-go

lawl. Totally missed everything under your code :D

Meh was worth it, Got to draw a bunny.

Think7 avatar Feb 08 '16 23:02 Think7

:+1: bunny

xealot avatar Feb 09 '16 00:02 xealot

:rabbit: :heart:

RyanCavanaugh avatar Feb 09 '16 00:02 RyanCavanaugh

+1, would definitely like to see this

nathan-rice avatar Mar 03 '16 15:03 nathan-rice

Have there been any discussion updates on this topic?

LPGhatguy avatar Mar 10 '16 23:03 LPGhatguy

It remains on our enormous suggestion backlog.

RyanCavanaugh avatar Mar 10 '16 23:03 RyanCavanaugh

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.

SylvainEstevez avatar Mar 14 '16 15:03 SylvainEstevez

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.

RonNewcomb avatar Apr 12 '16 01:04 RonNewcomb

This would provide a neat solution to the problem described in #8164.

yortus avatar Apr 20 '16 04:04 yortus

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.

iby avatar May 04 '16 18:05 iby

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.

felixfbecker avatar May 29 '16 08:05 felixfbecker

😰 Can't believe this still isn't fixed / added.....

Think7 avatar May 29 '16 08:05 Think7

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

PanayotCankov avatar Jul 19 '16 12:07 PanayotCankov

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

zpdDG4gta8XKpMCd avatar Jul 19 '16 12:07 zpdDG4gta8XKpMCd

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:

audinue avatar Jul 30 '16 00:07 audinue

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 avatar Jul 31 '16 22:07 shlomiassaf

@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 avatar Aug 01 '16 11:08 felixfbecker

@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.

shlomiassaf avatar Aug 01 '16 12:08 shlomiassaf

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 avatar Aug 01 '16 17:08 felixfbecker

@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!

LPGhatguy avatar Aug 01 '16 17:08 LPGhatguy

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.

felixfbecker avatar Aug 01 '16 18:08 felixfbecker

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

izatop avatar Aug 05 '16 16:08 izatop

You cannot use the this type on a static method, I think that's the entire root of the issue.

xealot avatar Aug 05 '16 16:08 xealot

@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?)

LPGhatguy avatar Aug 05 '16 17:08 LPGhatguy