TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

T.constructor should be of type T

Open LPGhatguy opened this issue 10 years ago • 92 comments

Given

class Example {
}

The current type of Example.constructor is Function, but I feel that it should be typeof Example instead. The use case for this is as follows:

I'd like to reference the current value of an overridden static property on the current class.

In TypeScript v1.5-beta, doing this requires:

class Example {
    static someProperty = "Hello, world!";

    constructor() {
        // Output overloaded value of someProperty, if it is overloaded.
        console.log(
            (<typeof Example>this.constructor).someProperty
        );
    }
}

class SubExample {
    static someProperty = "Overloaded! Hello world!";

    someMethod() {
        console.log(
            (<typeof SubExample>this.constructor).someProperty
        );
    }
}

After this proposal, the above block could be shortened to:

class Example {
    static someProperty = "Hello, world!";

    constructor() {
        // Output overloaded value of someProperty, if it is overloaded.
        console.log(
            this.constructor.someProperty
        );
    }
}

class SubExample {
    static someProperty = "Overloaded! Hello world!";

    someMethod() {
        console.log(
            this.constructor.someProperty
        );
    }
}

This removes a cast to the current class.

LPGhatguy avatar Jul 13 '15 17:07 LPGhatguy

Also, for clarity, as discussed in #4356 it is logical based on the ES specification to strongly type .constructor property of an instance and the following is essentially equivalent valid ways of creating instances:

class Foo {
    foo() { console.log('bar'); }
}

let foo1 = new Foo();

let foo2 = new foo1.constructor();

kitsonk avatar Sep 29 '15 08:09 kitsonk

Accepting PRs for this. Anyone interested? :smile:

RyanCavanaugh avatar Oct 05 '15 22:10 RyanCavanaugh

I spoke with @ahejlsberg about this - we could type constructor fairly well just with a change to our lib.d.ts now that we have this types (in theory):

interface Constructor<T> {
  new (...args: any[]): T;
  prototype: T;
}

interface Object {
    constructor: Constructor<this>;
}

But! There are two issues - we don't instantiate this types on apparent type members (as in, anything on object) correctly right now, and there would be a performance impact in making every object have a this typed member (it's constructor member). Additionally, this method doesn't capture the arguments of the constructor of the class (or its static members), so it could stand to be improved.

weswigham avatar Oct 05 '15 23:10 weswigham

Additionally, this method doesn't capture the arguments of the constructor of the class (or its static members),

This was a dealbreaker for us since it wouldn't even address the problem in the OP. Wiring it up in the compiler the same way we do prototype seems necessary.

RyanCavanaugh avatar Oct 05 '15 23:10 RyanCavanaugh

Can take a look, does

  1. Removing from lib.d.ts:
interface Object {
    constructor: Function;
  1. inferring/adding a "constructor" property/symbol with a type to the classType seem like the right thing to do?

jbondc avatar Oct 20 '15 13:10 jbondc

Nevermind, it's a bit more involved then I thought. Attempt here in case helpful: https://github.com/Microsoft/TypeScript/compare/master...jbondc:this.constructor

Think proper way involves changing ConstructorKeyword in getSymbolAtLocation()

jbondc avatar Oct 20 '15 15:10 jbondc

As I mentioned in #5933, will this not be a breaking change for assigning object literals to class types?

class Foo {
    constructor(public prop: string) { }
}

var foo: Foo = { prop: "5" };

This is currently allowed, but with this issue fixed it should produce an error that the literal is missing the constructor property? If it didn't produce an error, then it would be allowed to call statics on Foo such as foo.constructor.bar() which would break if foo.constructor wasn't actually Foo.

Arnavion avatar Dec 06 '15 18:12 Arnavion

Would this be possible to implement solely with the polymorphic this type that's a work in progress? Assuming typeof mechanics would work there as well, could the signature for Object.prototype.constructor be set to:

interface Object {
    constructor: typeof this;
}

LPGhatguy avatar Dec 09 '15 17:12 LPGhatguy

The general problem we face here is that lots of valid JavaScript code has derived constructors that aren't proper subtypes of their base constructors. In other words, the derived constructors "violate" the substitution principle on the static side of the class. For example:

class Base {
    constructor() { }  // No parameters
}

class Derived {
    constructor(x: number, y: number) { }  // x and y are required parameters
}

var factory: typeof Base = Derived;  // Error, Derived constructor signature incompatible
var x = new factory();

If we were to add a strongly typed constructor property to every class, then it would become an error to declare the Derived class above (we'd get the same error for the constructor property that we get for the factory variable above). Effectively we'd require all derived classes to have constructors that are compatible with (i.e. substitutable for) the base constructor. That's simply not feasible, not to mention that it would be a massive breaking change.

In cases where you do want substitutability you can manually declare a "constructor" property. There's an example here. I'm not sure we can do much better than that.

ahejlsberg avatar Dec 09 '15 18:12 ahejlsberg

Well, since the constructor, per spec (and after typescript compilation) is always the defined in the prototype, I don't see why it shouldn't be strongly typed. If people are hacking their prototypes manually, let them typecast their code. this.constructor should be the class definition, and super.constructor the immediate super prototype definition, and so on.

Since non-typescript modules usually have their manual typings (through DefinitelyTyped), it's safe to rewrite their constructor there as needed (in case the module author did something silly like):

function Foo(){
}
Foo.prototype = {
   constructor: Foo,
   someMethod: function() {
   }
}

pocesar avatar Jan 18 '16 05:01 pocesar

I don't understand what's going on in this thread. Need to take it back to the design meeting.

RyanCavanaugh avatar Jan 19 '16 19:01 RyanCavanaugh

Cant call static owerriden methods, if i write Animal.run() this is code not use Cat.run owerriden method. i need write this.constructor.run() but it get compilation error.

class Animal {

    static run (){
        alert('Animal run')
    }

    run(){
        this.constructor.run() // compile error
        // if i write Animal.run() this code not use Cat.run owerriden method
    }

}


class Cat extends Animal {

    static run (){
        super.run()
        alert('Cat jump')
    }

}

let cat = new Cat()
cat.run()

solution

add keyword static for access to current static props, static will be alias for this.constructor in self methods, and alias for this in static methods

and code will be:

class Animal {

    static run (){
        alert('Animal run')
    }

    run(){
        static.run() // compile to this.constructor.run()
    }

}


class Cat extends Animal {

    static run (){
        super.run()
        alert('Cat jump')
    }

}

let cat = new Cat()
cat.run() // alert('Animal run') alert('Cat jump')

dangers

keyword static can be to used in the next standards ES, and may have different semantics

P.S.

There is nothing wrong with adding a little sugar. This really is not anything bad. All restrictions that you come up with, to serve for the good of the people. And in this case, the failure to implement the keyword static, just from the fact that it is not in the standard, does more harm than good.

Language is a tool, and, as we understand, should be the keyword static. And we all know that.

TypeScript is a ES with types. ES not has static keyword, and we must not it have

This is nonsense and is an example of how not built right ideology hinders the improvement of living standards of people. The rule for the rule.

We need to add the keyword, since it will allow programmers to better to program. That's the purpose of the language. Am I wrong? Am I wrong???

uMaxmaxmaximus avatar Jun 24 '16 03:06 uMaxmaxmaximus

static.run() isn't valid javascript, in all senses. using an imaginary keyword that looks like a global. the Class specification does have static as a keyword http://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions one thing is transpiling code that works in ES5, another thing is changing the input to a completely different output. await / async for example are in the spec, and the generated code just makes you able to use ES2016 today. the coffeescript dark days are now past at last.

Typescript does one thing and does really well: having plain JS strongly-typed. they just need to fix the constructor to be strongly typed as well. I have no problem doing this.constructor.some() since using ClassDefinition.some() is awkward and makes inheritance a (even more) pain.

pocesar avatar Jul 19 '16 02:07 pocesar

Has anyone figured out some kind of workaround for this yet?

charrondev avatar Oct 13 '16 20:10 charrondev

Maybe use new keywords is better. For example:

In TypeScript:

class Foo {
  static base() {
    return 'FOO'
  }

  static foo() {
    return self.base()
  }

  static bar() {
    return static.base()
  }

  cfoo() {
    return self.base()
  }

  cbar() {
    return static.base()
  }
}

class Bar extends Foo {
  static base() {
    return 'BAR'
  }

  static more() {
    return parent.base()
  }

  cmore() {
    return parent.base()
  }
}

Expect executing result as follow:

Foo.base() // 'FOO'
Foo.foo() // 'FOO'
Foo.bar() // 'FOO'
Bar.base() // 'BAR'
Bar.foo() // 'FOO'
Bar.bar() // 'BAR'
Bar.more() // 'FOO'
const foo = new Foo()
foo.cfoo() // 'FOO'
foo.cbar() // 'FOO'
const bar = new Bar()
bar.cfoo() // 'FOO'
bar.cbar() // 'BAR'
bar.cmore() // 'FOO'

The three keywords self, static, parent means different access controls as follow:

  • self: access the constructor where the keyword is strictly
  • static: dynamic access the constructor according to the access context
  • parent: access the property of the super constructor of the constructor where the keyword is

The compile result is(omit useless codes):

(function (_super) {
  function _ctor() {}
  _ctor.foo = function() {
    _super.bar() // from parent.bar()
    _ctor.bar() // from self.bar()
    this.bar() // from static.bar()
  }

  _ctor.prototype.foo() {
    _super.bar() // from parent.bar()
    _ctor.bar() // from self.bar()
    this.constructor.bar() // from static.bar()
  }
})(function _super() {})

acrazing avatar Mar 21 '17 11:03 acrazing

@acrazing as already discussed in this thread that is a bad idea and it is contrary to the design goals of TypeScript.

To understand why it is a bad idea, consider that static already has a meaning in ECMAScript and that this meaning may expand. What if TC39 adds support for precisely this feature? What if it adds the same syntax with a different meaning? What is static is endowed with a meta-property like new was with target?TypeScript, as a JavaScript superset, would need to implement the new behavior of static. This would break existing code.

That is one reason to not add new syntax in value positions. It breaks compatibility.

aluanhaddad avatar Mar 31 '17 23:03 aluanhaddad

Possible workaround:

class Foo {
    'constructor': typeof Foo; // <-- this line here
    static method() { return 1 }
}
const foo = new Foo();
foo.constructor.method(); // Works
new foo.constructor(); // Works

Having an explicit opt-in isn't necessarily a bad thing either, since it makes it obvious that you're going to be using the .constructor property for something.

It seems a bit odd that the quotes are required, but just having constructor: typeof Foo will give you: [ts] Constructor implementation is missing. if you have no constructor implementation, or [ts] '(' expected. if you do.

As a bonus, you can use this to implement static properties on interfaces:

interface I {
    constructor: {
        staticMethod(): number;
    }
    instanceMethod(): string;
}

class C implements I {
    'constructor': typeof C;
    static staticMethod() { return 1 }
    instanceMethod() { return '' }
}

Chacha-26 avatar Apr 29 '17 23:04 Chacha-26

If someone wants to make it work with prettier (which removes the quotes), wrapping around [] does the trick:

class C {
    ['constructor']: typeof C;
}

albertogasparin avatar Oct 18 '17 11:10 albertogasparin

@ahejlsberg

Here we just wanna to use this.constructor to retrieve static member. If it must add "constructor": typeof XXX to every class, it also seems too unnecessary.

Since this.constructor have some unavoidable type problem, can we add a new grammer for this. For example: this::staticMember compiles to this.constructor.staticMember. Treat type of this::XXX as (typeof typeof this)['XXX']

Just like it in PHP: self::staticMember. It also use :: in C++.

k8w avatar Dec 27 '17 07:12 k8w

I agree with @k8w as introducing a new concept/keyword like constructof would be verbose!

As for the constructor.prototype, it cannot be strongly typed as may developers still use that means to add properties. Moreover, have you notice that though not suggested in autocomplete, when you do use it, no error is triggered?

SalathielGenese avatar Dec 27 '17 09:12 SalathielGenese

Any progress on this?

dbkaplun avatar Mar 10 '18 00:03 dbkaplun

I'm trying to call a static property from a Sequelize model as such:

const ModelT = options.target().constructor as typeof Model<T>;
return ModelT.count({ where });

options.target() return type is new () => T and T extends Model<T>

count is a static method of Model<T>, but I'm unable to type that cast properly. Any help would be appreciated.

alfaproject avatar Apr 08 '18 10:04 alfaproject

Looking at this now, having a built in polymorphic definition of this.constructor would probably do the trick. The biggest issue I have with this:

class Foo {
    'constructor': typeof Foo; // <-- this line here
    static method() { return 1 }
}
const foo = new Foo();
foo.constructor.method(); // Works
new foo.constructor(); // Works

Is that it's not polymorphic. For example the above falls apart when extending classes.

class Foo {
    'constructor': typeof Foo; // <-- this line here
    static method() { return 1 }
}
class FooChild extends Foo {
}
const foo = new Foo();
const fooChild = new FooChild();

foo.constructor.method(); // Works
new foo.constructor(); // Works

fooChild.constructor.method(); // Doesn't Work
new fooChild.constructor(); // Doesn't Work

charrondev avatar Apr 08 '18 19:04 charrondev

Relevant ECMAScript proposal: https://github.com/rbuckton/proposal-class-access-expressions

KSXGitHub avatar May 28 '18 10:05 KSXGitHub

It's true what @ahejlsberg's comment here about type incompatibility which will happen if every object has different types of constructor property.

However, after thinking for a while, I think we were providing a little less information than we could provide. Well, I will explain my idea for this issue.

constructor type information from the new operator

type Instance<T, P> = T & { constructor: P }

Instance<T, P> augments T type with the type information of constructor property. We can provide the information somewhere and I think the sweet spot is at the return value of the new operator. Look at the example below. (open in TypeScript playground here)

class Class1 {
  constructor(value: number) {
  }
}

class Class2 extends Class1 {
  constructor(value: string) {
    super(Number(value))
  }
}

let i1 = new Class1(100) as Instance<Class1, typeof Class1>
let i2 = new Class2("1") as Instance<Class2, typeof Class2>
//                       ^ this is what I propose: make this auto-magically happens

i1 = i2               // <-- error because incompatible constructors

let di1: Class1 = i1  // <-- let's reduce information that we have here
let di2: Class2 = i2

di1 = di2             // <-- not even an error happens here :)

This idea will solve the problem here but still allows for more specific typing if needed.

Sidenote

The behavior of returning T from new operator has been around there for a long time. I'm thinking about automagically reduce the information at assignment operation if the variable type not explicitly stated.

let s1 = new Class1(100)  // <-- ReducibleInstance<Class1, typeof Class1>
let s2 = new Class2("1")  // <-- ReducibleInstance<Class2, typeof Class2>

// s1.constructor.           <-- resolve to typeof Class1

s1 = s2   // <-- reduce s1 to Class1 before assignment only if signature is incompatible
          //     s2 is still typed as ReducibleInstance<Class2, typeof Class2>

// s1.constructor.           <-- now resolve to Function

nieltg avatar Jun 18 '18 18:06 nieltg

Well, I'm not finished yet. This alone is not enough to solve the whole problem. Therefore, I'll propose another design idea.

Polymorphic typeof this type

I have an idea to create a new type named typeof this type which is similar to polymorphic this type but for the type of the class instead of for the instance. This is similar to @LPGhatguy comment here. I'll highlight some traits of my concept idea below.

Dissolve to typeof T from the outside

Like polymorphic this type which dissolves to T when accessed from the outside, polymorphic typeof this type dissolves to typeof T when accessed from the outside.

class Class3 {
  method1(): this {         // <-- type: this
    return this
  }

  method2(): typeof this {  // <-- type: typeof this
    return this.constructor
  }
}

let t1 = new Class3()
t1.method1()  // <-- type: Class3
t1.method2()  // <-- type: typeof Class3

Resolve currently available members of the class type from the inside

Like polymorphic this type which resolves currently available members when accessed from the inside, polymorphic typeof this resolves currently available static members of the class.

class Class4 {
  static staticProp1: number
  prop1: number

  method1() {
    let obj = this              // <-- type: this
    // obj.                        <-- resolve prop1, method1

    let cls = this.constructor  // <-- type: typeof this
    // cls.                        <-- resolve to staticProp1, etc
  }
}

class Class5 extends Class4 {
  static staticProp2: number
  prop2: number

  method2() {
    let obj = this              // <-- type: this
    // obj.                        <-- resolve prop1, prop2, method1, method2

    let cls = this.constructor  // <-- type: typeof this
    // cls.                        <-- resolve to staticProp1, staticProp2, etc
  }
}

Don't resolve any construct signatures from the inside

Reason: child's construct signature is allowed to be incompatible with parent's construct signature. We don't have enough information to figure out the signature of the final constructor at the moment.

class Class6 {
  constructor(readonly value: number) {
  }

  method1() {
    new this.constructor(17)  // <-- Error TS2351: Cannot use 'new' with an ...
  }
}

class Class7 extends Class6 {
  constructor(value: string) {
    if (typeof value !== "string") {
      throw new Error("value must be a string")
    }

    super(Number(value))
  }

  // method1() {
  //   new this.constructor(17)  <-- this expr. will break the rule if allowed
  // }
}

Provided by this type

Since we're not changing the type of Object.prototype.constructor, we have to provide access to this feature. I'm thinking about putting this on this type.

class Class8 {
  method1() {
    // this.constructor.   <-- resolve to typeof this
  }
}

let it1: Class8 = new Class8()
// it1.constructor.        <-- resolve to Function

Motivating Problem to Solve

My actual problem is I want to create an extendable class that leverages immutable fluent interface pattern. I took an example from TypeScript documentation here with a few changes. (open in playground)

class Calculator {
  constructor(readonly value = 0) {
  }

  protected newInstance(value: number): this {
    return new (this.constructor as any)(value)   // <-- not type-safe
  }

  add(operand: number) {
    return this.newInstance(this.value + operand)
  }

  // ... other operations go here
}

let v = new Calculator(2)
  .add(1)
  .value

One of my objectives is I want to make this code more type-safe since the constructor's type signature is not checked yet.

It will be possible to resolve to resolve static members of the class from this.constructor attribute and to do type-checking on the constructor of the child class from the parent class using this idea by modifying the Calculator class above to be like this.

class Calculator {
  // ...

  protected newInstance(
    this: this & { constructor: { new(value: number) } },
    value: number
  ): this {
    return new this.constructor(value)
  }

  add(
    this: this & { constructor: { new(value: number) } },
    operand: number
  ) {
    return this.newInstance(this.value + operand)
  }

  // ... other operations go here
}

Every method which associates with the fluent interface will not be able to be called from an instance of the class which doesn't have the required signature.

class BrokenCalculator extends Calculator {
  constructor(value: string) {
    super()
  }
}

let v2 = new BrokenCalculator()
  .add(5)   // <-- this resolves to:
            //     ReducibleInstance<BrokenCalculator, typeof BrokenCalculator>
            //     (error: not compatible with required this by the method)

Thank you. I'm looking forward to hearing about this. I'm sorry for long post here.

I hope this idea will be able to solve our problems. 😃

nieltg avatar Jun 18 '18 18:06 nieltg

It's quite boring to write ['constructor']: typeof FooClass every time so I think it worth to fix this.

Solution must be simple. The following code placed inside of the class constructor or its method

...
   this.constructor
...

is always pointing to the class' variable unless someone failed to assign Foo.prototype.constructor = property by Parent class during inheritance.

hinell avatar Oct 16 '18 09:10 hinell

Any news on this?

Friksel avatar Dec 27 '18 09:12 Friksel

This would be nice to have! It's a very common case. There are often libs that rely on settings being specified on classes (and extended in subclasses), and those libs retrieve the settings via this.constructor.someSetting.

@ahejlsberg

The general problem we face here is that lots of valid JavaScript code has derived constructors that aren't proper subtypes of their base constructors. In other words, the derived constructors "violate" the substitution principle on the static side of the class.

That's true, but has no bearing on the fact that when we read this.constructor ~~we intuitively expect a value of the type of this's constructor assuming it was defined with class. That case can be satisfied.~~ we don't know which subclass this will be made from.

Now, what we do with the constructor after we've read its value is a different story. Maybe there needs to be two type checks: one prevents it from being substitutable when called (typed as Function), and the other just treats it as a static access when reading properties. Calling new this.constructor is not usual, but reading this.constructor.foo is common.

trusktr avatar May 09 '19 03:05 trusktr

I was OK with adding field 'constructor': typeof ClassName but after upgrading to TS 3.5.1 I receive error Classes may not have a field named 'constructor'. It is really obtrusive that this issue is open for 4 years :)

ayZagen avatar Jun 10 '19 21:06 ayZagen