Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

T.constructor should be of type T #3841

Open
LPGhatguy opened this issue Jul 13, 2015 · 90 comments
Open

T.constructor should be of type T #3841

LPGhatguy opened this issue Jul 13, 2015 · 90 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@LPGhatguy
Copy link
Contributor

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.

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Jul 13, 2015
@kitsonk
Copy link
Contributor

kitsonk commented Sep 29, 2015

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

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". and removed In Discussion Not yet reached consensus labels Oct 5, 2015
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Oct 5, 2015
@RyanCavanaugh
Copy link
Member

Accepting PRs for this. Anyone interested? 😄

@weswigham
Copy link
Member

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.

@RyanCavanaugh
Copy link
Member

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.

@jbondc
Copy link
Contributor

jbondc commented Oct 20, 2015

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
Copy link
Contributor

jbondc commented Oct 20, 2015

Nevermind, it's a bit more involved then I thought. Attempt here in case helpful:
master...jbondc:this.constructor

Think proper way involves changing ConstructorKeyword in getSymbolAtLocation()

@Arnavion
Copy link
Contributor

Arnavion commented Dec 6, 2015

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.

@LPGhatguy
Copy link
Contributor Author

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;
}

@ahejlsberg
Copy link
Member

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.

@pocesar
Copy link

pocesar commented Jan 18, 2016

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() {
   }
}

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Help Wanted You can do this Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". labels Jan 19, 2016
@RyanCavanaugh RyanCavanaugh removed this from the Community milestone Jan 19, 2016
@RyanCavanaugh
Copy link
Member

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

@Fryuni
Copy link

Fryuni commented Sep 21, 2022

I just reached this discussion, so I wanted to share a clean type safe solution, so long as you don't mind an extra call:

Add this somewhere that you find convenient.

Object.prototype.static = function static(): any {
    return this.constructor;
};

declare global {
    interface Object {
        static<T extends NewableFunction>(this: InstanceType<T>): T;
    }
}

Then you can use static() just like how constructor should work:

class Parent {
  static staticMethod() { console.log('From parent'); }
  instanceMethod() {
    this.static().staticMethod();
  }
}

class Child {
  static staticMethod() { console.log('From child'); }
}

new Child().instanceMethod(); // Calls the child static method

The T extends NewableFunction is to disallow using this on object literals and the classes themselves, that method should only be used on instances.

At first I thought that just making this solution a getter withget static would give the intended behavior, but the type magic with the this: InstanceType<T> doesn't work on getters. If it did, we could just replace the declaration with get constructor and it would work.

@Fryuni
Copy link

Fryuni commented Sep 21, 2022

If TypeScript changes the behavior of getters to support typing this, then replacing the definition of constructor on the lib should be simple. But I don't know how difficult it would be to change the getters.

@clshortfuse
Copy link

To add on to the solutions, I've been doing this:

class SuperClass {
  get static() {
    return /** @type {typeof SuperClass} */ (/** @type {unknown} */ (this.constructor));
  }
}

class SubClass extends SuperClass {
   get static() { return /** @type {typeof SubClass} */ (super.static); }
}

You cannot do typeof this, as tempting as it sounds. You have to do typeof ${className}. From there, within an instance, call this.static.name and you'll get it like you normally word. (eg: this.static.observedAttributes, myObject.static.name). I use .static because IMO it's clearer than .constructor.

@Evertt
Copy link

Evertt commented Nov 9, 2022

@Fryuni I tried your solution, since it looked the most promising, because I'm looking for one global solution.
But I get an error when I try it:

image

And indeed when I inspect NewableFunction, it doesn't contain a new (...args: any): any signature or anything similar. Are you using a specific compiler configuration in your tsconfig.json that makes this work for you?

@Fryuni
Copy link

Fryuni commented Nov 9, 2022

Hey @Evertt, sorry I was oversimplifying here when copying from our actual code without much care.
NewableFunction is to allow using instanceof properly.

For InstanceType we defined a Constructable, which frankly should probably be in the std lib for symmetry with InstanceType:

type Constructable = abstract new(...args: any[]) => any;

declare global {
    interface Object {
        static<T extends Constructable>(this: InstanceType<T>): T;
    }
}

Object.prototype.static = function(): any {
    return this.constructor;
};

In the few places where we used this (and we don't anymore), we actually assigned to a variable with the proper type, but it also works by passing a type argument:

class Parent {
  static staticMethod() { console.log('From parent'); }
  public instanceMethod() {
    const klass: typeof Parent = this.static()
    
    klass.staticMethod();

    // OR

    this.static<typeof Parent>().staticMethod();
  }
}

class Child extends Parent {
  static staticMethod() { console.log('From child'); }
}

new Child().instanceMethod(); // Calls the child static method

And those truly validate that the type is either the class or a parent of the class, as you can see here.

We later realized that this actually isn't sound because TypeScript only checks the prototype chain when the parent contains private or protected instance values/methods. If there are none, TypeScript treats the class as an interface and only checks that both classes have the same structure. See here.

This was part of a unrelated, but quite surprising discovery for us:

class Foo {}

function foo(value: Foo) {
  if (value instanceof Foo) return;

  // This is _not_ unreachable!
  throw new Error();
}

// This works
foo('something');

Which means you can create this level of blasphemy

@Evertt
Copy link

Evertt commented Nov 10, 2022

@Fryuni ah that's too bad. The fact that you need to manually cast the return value of static() to typeof Parent already makes this method useless to me. I was hoping to find a way to write the interface for static() such that TypeScript would always be able to correctly infer its return type.

@hsghn
Copy link

hsghn commented Nov 19, 2022

Yes

@hinell
Copy link

hinell commented Nov 20, 2022

@Evertt The best solution so far was given here: #3841 (comment)

@dpraul
Copy link

dpraul commented Dec 13, 2022

We were using the solution in #3841 (comment), but now that we are using "strictPropertyInitialization": true in our tsconfig.json we have to add the definite assignment operator:

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

@DetachHead
Copy link
Contributor

DetachHead commented Jan 4, 2023

the workaround i went with is to create a function that instantiates a class and modifies the constructor type:

type Class = new (...args: any[]) => any

type HasTypedConstructor<T extends Class> = InstanceType<T> & { 'constructor': T }

const New = <T extends Class>(class_: T, ...args: ConstructorParameters<T>): HasTypedConstructor<T> => new class_(args)

class Foo {
    constructor(public a: number) { }
}

const foo = New(Foo, 1)

foo.constructor // typeof Foo

playground

btw i've also added it to my ts-helpers package:

import { New } from '@detachhead/ts-helpers/dist/functions/misc'

@TechQuery
Copy link

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

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

As the new confused proposal "class fields as define properties" of ES2022 becomes TypeScript default behavior, we must write as following to avoid access-blocking to the prototype:

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

@hinell
Copy link

hinell commented Mar 29, 2023

@TechQuery This issue didn't age well as I see. Now even more crowded, completely unnecessary keywords are needed. What a joke. This is going to break backwards compatibility again. This is crazy. I think TS needs to be replaced by more robust and sensible language.

@datvm
Copy link

datvm commented Jun 5, 2023

Any workaround for annonymous class without manually giving it a name?

new class {

    static #init = false;

    constructor() {
        (this.constructor as ???).#init = true; // What to put in here?
    }

}();

@ljharb
Copy link
Contributor

ljharb commented Jun 5, 2023

the workaround is to give it a name, since anonymous things hurt debugging anyways.

@hinell
Copy link

hinell commented Jun 6, 2023

The workaround is to drop TypeScript altogehter. We need a better language. Read:

@ShaMan123
Copy link

ShaMan123 commented Jun 6, 2023

That should be a great concern and alarming to the TS team.
As well as this issue and the need to do horrible workarounds to do things that are considered the core scope of TS.
TS is there to make coding easier, not harder.
All the workarounds in this thread are too ugly, forgive me if that offends anyone.

@pinko-fowle
Copy link

(this.constructor as MyClass).myStaticMethod() is the workaround I've been using. Quite dislike it.

Here's an example use case:

var A = class {
        static answer(): any { return 42 };
        lifeUniverseEverything() { return (this.constructor as typeof A).answer() } // WORKAROUND HERE
}
var B = class extends A {
        static answer(){ return "6*9" }
}
console.log(`the answer to life the universe & everything is... ${(new B).lifeUniverseEverything()}`)
// => the answer to life the universe & everything is... 6*9

Being able to have virtual static methods is a super-powerful & super-useful capabillity! Wish it was better supported. Thanks, happy hacking folks.

@clshortfuse
Copy link

clshortfuse commented Sep 9, 2023

@pinko-fowle Note that doing so regenerates the entire class, with the equivalent of

{
  new (...args: any[]): {[K in key of CLASS]: CLASS[K]},
  [K in key of typeof CLASS]: typeof CLASS[K],
} & typeof CLASS;

That makes a really gigantic file when you generate types since it's duplicating every single key. The more you extend the worse it gets. Something like HTMLElement has 100s of properties.

@trusktr
Copy link

trusktr commented Sep 21, 2023

@Fryuni I can't get your example to work.

playground

Screenshot 2023-09-20 at 9 51 25 PM

@Fryuni
Copy link

Fryuni commented Sep 21, 2023

@trusktr, there are three problems:

The first one is somewhat silly, but you need the export {} somewhere at the top level of the file for the declare global to work correctly. Don't ask me why, I never figured that one out.

Second, my solution cannot infer the type; just validate it. demonstration.

The last one I mentioned in this comment, when the class has no private instance members, any other class with a subset of the instance members will be assignable to it. It worked for us because all our classes do have private members. demonstration

@clshortfuse
Copy link

@trusktr If you're okay with not using native syntax for JS extends you can do this:

class TSConstructorFix {
  /**
   * @type {{
   * <T1 extends typeof TSConstructorFix, T2 extends T1, T3 extends (Base:T1) => T2>
   * (this: T1,customExtender: T3):
   *  ReturnType<T3>
   *      & {
   *          new(): {
   *              static(): {[K in keyof ReturnType<T3>]: ReturnType<T3>[K]}
   *          } & {
   *            [K in keyof InstanceType<ReturnType<T3>>]: InstanceType<ReturnType<T3>>[K]}
   *          }
   *      }
   * }}
   */
  static extend(customExtender) {
    // @ts-expect-error Can't cast T
    return customExtender(this);
  }

  static() {
    return this.constructor;
  }
}

const Test = TSConstructorFix.extend((Base) => class extends Base {
  static foo = 123;

  go() { return 0; }
});

const t = new Test();
t.static().foo;
t.go();

Playground link

That's basically a fork of how I've workaround this issue. I'm kinda used to that more declarative syntax now, personally.

Base class

Extension Example

@clshortfuse
Copy link

clshortfuse commented Sep 21, 2023

So this works...

class TSConstructorFix {
  /**
   * @type {{
   * <T1 extends typeof TSConstructorFix, T2 extends T1, T3 extends (Base:T1) => T2>
   * (this: T1,customExtender: T3):
   *  ReturnType<T3>
   *      & {
   *          new(...args: any[]): {
   *              constructor: {[K in keyof ReturnType<T3>]: ReturnType<T3>[K]}
   *          } & {
   *            [K in keyof InstanceType<ReturnType<T3>>]: InstanceType<ReturnType<T3>>[K]}
   *          }
   *      }
   * }}
   */
  static extend(customExtender) {
    // @ts-expect-error Can't cast T
    return customExtender(this);
  }
}
- class Test {
+ class Test extends TSConstructorFix.extend((Base) => class extends Base {
  static foo = 123;

  go() { return 0; }
- }
+ }) {}
const t = new Test();
t.constructor.foo += 4;
t.go();

If you're willing to add extends TSConstructorFix.extend((Base) => class extends Base after every class name and ]) { at the end (maybe a Regex). There's more probably some more coaxing of ConstructorParameters<T> left, but that's the main idea.

@trusktr
Copy link

trusktr commented Sep 22, 2023

@clshortfuse that'd be a fairly invasive change I think. Better to just const ctor = this.constructor as Foo in some few spots I think.

@Fryuni Thanks, got it! Btw, export {} not needed if you tack the export onto one of your actual classes (that's what I had in my example).

@teadrinker2015
Copy link

the workaround is to give it a name, since anonymous things hurt debugging anyways.

Considering calling your parents and your friends in their full names. It's more explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests