-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Polymorphic "this" for static members #5863
Comments
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:
|
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 |
@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.
|
@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. |
👍 bunny |
🐰 ❤️ |
+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 |
I'm working on the typings for Sequelize 4.0 and it uses an approach where you subclass a 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 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..... |
I found a similar issue here Working in typescript:
Not with jsdoc:
To my surprise there seems to be no way to do it other than a hack like below. I am very tempted to convert all our 400k+ loc from javascript to typescript in one go using ts-migrate, however, I still cannot justify all the potential work ahead just to make the compiler happy, this is why I have chosen the jsdoc approach as an precursor step.
|
+1 for this feature as well. In the meantime, @tao-cumplido's unknown prototype trick worked for me: class Parent {
static create<T extends { prototype: unknown } = any>(this: T): T["prototype"] {
return new (this as any)();
}
}
class Child extends Parent {}
const child = Child.create(); |
``I can solved this problem with special custom typings. Maybe we have reason to include this in new version Typescript typings for all. type ThisConstructor<
T extends { prototype: unknown } = { prototype: unknown },
> = T;
type This<T extends ThisConstructor> = T['prototype'];
export class Animal {
static getInstance<T extends ThisConstructor<typeof Animal>>(
this: T,
): This<T> {
return new this();
}
static getClass<T extends ThisConstructor<typeof Animal>>(
this: T,
): ThisConstructor<T> {
return this;
}
public name: string;
}
export class Cat extends Animal {
static getEmptyCat() {
return new this();
}
static getEmptyCatFromStatic() {
return this.getInstance();
}
meow() {
return true;
}
}
const animal = Animal.getInstance(); // [PARENT] Animal - correct
const animalClass = Animal.getClass(); // [PARENT] typeof Animal - correct
const cat1 = Cat.getEmptyCat(); // [CHILD] Cat (as default) - correct;
const cat2 = Cat.getEmptyCatFromStatic(); // [CHILD] Cat - correct
const cat3 = Cat.getInstance(); // [CHILD] Cat - correct
const cat4 = Cat.getClass(); // [CHILD] typeof Cat - correct |
Here's what I'm doing for Subclass (extension): /**
* @template S Superclass
* @template {S} T Class
* @typedef { Omit<S, keyof T> & {
* new (...args:ConstructorParameters<T>): (InstanceType<T>)
* } & {
* prototype: InstanceType<T>
* } & {
* [P in keyof T]: T[P] extends Function
* ? (ReturnType<T[P]> extends S
* ? (ReturnType<T[P]> extends T
* ? T[P]
* : (
* this: (ThisParameterType<T[P]> extends S
* ? (ThisParameterType<T[P]> extends T
* ? ThisParameterType<T[P]>
* : SubclassOf<ThisParameterType<T[P]>,T>)
* : ThisParameterType<T[P]>),
* ...args:Parameters<T[P]>
* ) => SubclassOf<ReturnType<T[P]>,T>)
* : T[P])
* : T[P]
* }} SubclassOf
*/
/**
* @template S
* @template C
* @param {S} SuperClass
* @param {C} Class
* @return {SubclassOf<S,C>}
*/
const CastSubclassOf = (SuperClass, Class) => Class; It works pretty well, and I have a related Thanks to @wenq1 for the idea of using a JS function to force a TS cast. That helps get around the fact you can't respec any object as a class in TS. So, I use this to cast an Object as a Class: /**
* @template T
* @typedef {{
* new (...args:ConstructorParameters<T>): InstanceType<T>
* } & {
* prototype: InstanceType<T>
* } & {
* [P in keyof T]:T[P]
* }} ClassOf
*/
/**
* @template T
* @param {T} Class
* @return {ClassOf<T>}
*/
const CastClassOf = (Class) => Class; You can do |
Apparently there's a JSDocs bug somewhere, because the JS version @berish-ceo 's sample doesn't work right. That explains why I've been having so much trouble: /**
* @template {ThisConstructor<typeof Animal>} [T=ThisConstructor<typeof Animal>]
* @typedef {T} ThisConstructor
*/
export class Animal {
/**
* @template {ThisConstructor<typeof Animal>} T
* @this {T}
* @returns {ThisConstructor<T>}
*/
static getClass() {
return this;
}
}
export class Cat extends Animal {
}
const animalClass = Animal.getClass(); // [PARENT] typeof Animal - correct
const cat4 = Cat.getClass(); // [CHILD] typeof Animal - *incorrect* |
Before that I found a solution, but it turned out to be not ideal in complex scenarios in practice in my projects. Looking for a better solution in one of our multiple inheritance architectures, we were able to find a better fit using Generics. Previous SolutionJust in case, I leave a link to my previous solution, suddenly the old version will be useful to someone. But as for me, the new version copes with scripts much better (in a complex production project). Prev solution commented on Dec 3, 2022 New SolutionTo begin with, we have defined special individual types.
Why such a division? Because there are classes that use a private constructor, and in this case, the PrototypeType will help. In normal scenarios, the ConstructorFunctionType takes precedence because it allows you to create instance.
// this.typings.ts
export interface PrototypeType<T> extends Function {
prototype: T;
}
export interface ConstructorFunctionType<T = any> extends PrototypeType<T> {
new (...args: any[]): T;
}
export type ConstructorType<T = unknown, Static extends Record<string, any> = PrototypeType<T>> = (ConstructorFunctionType<T> | PrototypeType<T>) & {
[Key in keyof Static]: Static[Key];
}; Use case in practiceI will write a minimal practical case how to use it // value-object.ts
export abstract class ValueObject {
static isExtends<T extends ValueObject>(this: ConstructorType<T, typeof ValueObject>, instance: any): instance is T {
return !!instance && instance instanceof this; // example
}
...
}
// specification.ts
export class Specification<T> extends ValueObject {
...
}
// 1-example.ts
const abc: unknown = ...;
if(Specification.isExtends(abc)) {
// abc : Specification<...>
} Overriding scenarioThere is also a scenario when we OVERRIDE the functionality of static functions in children, then use default overriding Default functionality // entity.ts
export abstract class Entity {
static create<T extends Entity>(this: ConstructorType<T, typeof Entity>): T {
return ...
}
...
}
// user.ts
export class User extends Entity {
...
}
// Entity.create() => Enttiy
// User.create() => User -> without overloading And example with custom overriding // product.ts
export class Product extends Entity {
static override create<T extends Product>(this: ConstructorType<T, typeof Product>): T;
static override create<T extends Entity>(this: ConstructorType<T, typeof Entity>): T;
static override create<T extends Product>(this: ConstructorType<T, typeof Product>): T {
return super.create(); // or custom logic
}
...
} ConclusionIf you have questions, I am ready to answer new questions in my free time. In our example, all difficult cases are generally closed for us. Now we have a direct generic from type, and not . And for us this is a victory :) |
@berish-ceo Great work! Any thoughts on how
|
Happy 8 year anniversary, my favorite TypeScript issue! |
😤 this is not a laughing matter 😤 |
As of today, the following solution works just fine: export class Entity<T extends object> {
data: T;
constructor(data: T) {
this.data = data;
}
static create<T extends object, U extends Entity<T>>(this: new (data: T) => U, data: T) {
return new this(data);
}
}
interface TypeUser {
id: string
name: string
}
export class User extends Entity<TypeUser> {
}
const user = User.create({ id: '1', name: 'John' }); Similarly in JSDoc: /**
* @template {object} T
*/
export class Entity {
/**
* @param {T} data
*/
constructor(data) {
this.data = data;
}
/**
* @template {object} T
* @template {Entity<T>} U
* @this {new (data: T) => U}
* @param {T} data
* @returns {U}
*/
static create(data) {
return new this(data);
}
}
/**
* @typedef {object} TypeUser
* @property {string} id
* @property {string} name
*/
/**
* @extends {Entity<TypeUser>}
*/
export class User extends Entity {
}
const user = User.create({ id: '1', name: 'John' }); Playground: |
There is no need to make base class generic. Similar pattern works in class Base {
static create<Type extends Base>(
this: { new (): Type },
// ...
): Type {
return new this(/* ... */);
}
}
class User extends Base {
}
User.create(); |
@alexeyraspopov You're right. I was a little bit biased by my own use-case which I was trying to solve.
Probably no need if you only want to demonstrate the polymorphism of |
@RobertAKARobin in case you have not found a solution, here is how i solved static create method with typed this and parameters: class Entity {
static create<O extends typeof Entity>(this: O, ...options: ConstructorParameters<O>): InstanceType<O> {
const entity = new this(
// @ts-expect-error Was not able to make this without error, but types are working just fine!
...options,
)
return entity as unknown as InstanceType<O>
}
constructor(options: { someValue: boolean }, key: string) {}
method() {}
}
class SubEntity extends Entity {
constructor(options: { someValue: boolean; subValue: string }, key: string) {
super(options, key)
}
}
Entity.create({ someValue: true }, 'key').method()
SubEntity.create({ someValue: true, subValue: 'yay!' }, 'key').method() |
While this issue is annoying, I've settled on a somewhat reasonable solution. I feel it could be better with language improvements, but when life gives you lemons 🍋 make lemonade. The
abstract class CarbonReact<P = {}, S extends iCarbonReactState = iCarbonReactState> extends Component<{
children?: ReactNode | ReactNode[],
instanceId?: string,
} & P, S> {
context: Context<S & iCarbonReactState> = createContext(this.state);
protected target: typeof CarbonReact;
private static _instance: ThisType<CarbonReact<any, any>>;
static getInstance<T extends CarbonReact<any, any>>(): T {
return this._instance as T;
}
static get instance() {
return this.getInstance();
}
static set instance(instance: CarbonReact<any, any>) {
this._instance = instance;
}
protected constructor(props: {
children?: ReactNode | ReactNode[];
instanceId?: string; // Optional instanceId from props
} & P) {
super(props);
console.log('CarbonORM TSX CONSTRUCTOR');
Object.assign(new.target, {
_instance: this,
instance: this,
});
}
} export default class ChildClass extends CarbonReact<{}, typeof initialCarbonORMState> {
static instance: ChildClass;
state = initialCarbonORMState;
constructor(props) {
super(props);
ChildClass.instance = this;
}
} |
One downside is you can't make the constructor private or protected. |
I tried copy your code to VSCode, it works well. But I cannot understand the code follows. Const c is inferred to class BaseCounter<K> extends Map<K, number> {
public get(key: K): number {
return super.get(key) ?? 0;
}
public static fromKeys<K, T extends BaseCounter<K>>(this: new () => T, keys: Iterable<K>): T {
return new this().addFromIterable(keys);
}
public addFromIterable(iterableKeys: Iterable<K>): this {
for (const key of iterableKeys) {
super.set(key, this.get(key) + 1);
}
return this;
}
}
class ChildCounter<S> extends BaseCounter<S> {
}
const c = ChildCounter.fromKeys('hello'); |
Can this please be prioritized? 🙏🤞 class Base {
static defineElement(): this { // expected no error
return this
}
}
class Foo extends Base {
foo = 123
}
const b = Base.defineElement() // type of b is "any", expected "typeof Base"
console.log(b === Base) // logs "true"
const f = Foo.defineElement() // type of f is "any", expected "typeof Foo"
console.log(f === Foo) // logs "true" |
I would really love for this feature to be implemented. I have a use case that is not related to instantiation and is not addressed by the suggested workarounds: class Base {
static lookup = {}
readonly transformedLookup: Record<keyof typeof static this['lookup'], boolean> // :(
}
class Derived extends Base {
static lookup = { foo: "expensive computation", bar: "expensive computation" }
}
new Derived().transformedLookup.bar // :( The code will work if I remove the |
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 theBaseModel
that comes back from theFactory
you should be able to override it. However this fails becausethis
cannot be used in a static member.Maybe this issue is silly and there is an easy workaround. I welcome comments regarding how such a pattern can be achieved.
The text was updated successfully, but these errors were encountered: