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
Comments
Also, for clarity, as discussed in #4356 it is logical based on the ES specification to strongly type class Foo {
foo() { console.log('bar'); }
}
let foo1 = new Foo();
let foo2 = new foo1.constructor(); |
Accepting PRs for this. Anyone interested? 😄 |
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 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. |
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 |
Can take a look, does
|
Nevermind, it's a bit more involved then I thought. Attempt here in case helpful: Think proper way involves changing |
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 |
Would this be possible to implement solely with the polymorphic interface Object {
constructor: typeof this;
} |
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 In cases where you do want substitutability you can manually declare a |
Well, since the 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() {
}
} |
I don't understand what's going on in this thread. Need to take it back to the design meeting. |
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 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 At first I thought that just making this solution a getter with |
If TypeScript changes the behavior of getters to support typing |
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 |
@Fryuni I tried your solution, since it looked the most promising, because I'm looking for one global solution. And indeed when I inspect |
Hey @Evertt, sorry I was oversimplifying here when copying from our actual code without much care. For 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 |
@Fryuni ah that's too bad. The fact that you need to manually cast the return value of |
Yes |
@Evertt The best solution so far was given here: #3841 (comment) |
We were using the solution in #3841 (comment), but now that we are using class C {
['constructor']!: typeof C;
} |
the workaround i went with is to create a function that instantiates a class and modifies the 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 btw i've also added it to my import { New } from '@detachhead/ts-helpers/dist/functions/misc' |
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;
} |
@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. |
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?
}
}(); |
the workaround is to give it a name, since anonymous things hurt debugging anyways. |
The workaround is to drop TypeScript altogehter. We need a better language. Read: |
That should be a great concern and alarming to the TS team. |
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. |
@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 |
@Fryuni I can't get your example to work. |
@trusktr, there are three problems: The first one is somewhat silly, but you need the 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 |
@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(); That's basically a fork of how I've workaround this issue. I'm kinda used to that more declarative syntax now, personally. |
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 |
@clshortfuse that'd be a fairly invasive change I think. Better to just @Fryuni Thanks, got it! Btw, |
Considering calling your parents and your friends in their full names. It's more explicit. |
Given
The current type of
Example.constructor
isFunction
, but I feel that it should betypeof 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:
After this proposal, the above block could be shortened to:
This removes a cast to the current class.
The text was updated successfully, but these errors were encountered: