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

Polymorphic "this" for static members #5863

Open
xealot opened this issue Dec 1, 2015 · 194 comments
Open

Polymorphic "this" for static members #5863

xealot opened this issue Dec 1, 2015 · 194 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@xealot
Copy link

xealot commented Dec 1, 2015

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.

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Dec 1, 2015
@Think7
Copy link

Think7 commented Dec 17, 2015

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.

@LPGhatguy
Copy link
Contributor

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

@paul-go
Copy link

paul-go commented Feb 8, 2016

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.

@RyanCavanaugh
Copy link
Member

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

@Think7
Copy link

Think7 commented Feb 8, 2016

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


@paul-go
Copy link

paul-go commented Feb 8, 2016

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

@Think7
Copy link

Think7 commented Feb 8, 2016

lawl. Totally missed everything under your code :D

Meh was worth it, Got to draw a bunny.

@xealot
Copy link
Author

xealot commented Feb 9, 2016

👍 bunny

@RyanCavanaugh
Copy link
Member

🐰 ❤️

@nathan-rice
Copy link

+1, would definitely like to see this

@LPGhatguy
Copy link
Contributor

Have there been any discussion updates on this topic?

@RyanCavanaugh
Copy link
Member

It remains on our enormous suggestion backlog.

@SylvainEstevez
Copy link

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.

@RonNewcomb
Copy link

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.

@yortus
Copy link
Contributor

yortus commented Apr 20, 2016

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

@iby
Copy link

iby commented May 4, 2016

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.

@felixfbecker
Copy link
Contributor

felixfbecker commented May 29, 2016

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.

@Think7
Copy link

Think7 commented May 29, 2016

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

@wenq1
Copy link

wenq1 commented May 5, 2022

I found a similar issue here
link. Works in typescript, but not in jsdoc

Working in typescript:

class DataObjectBase {
    static fieldNames: Record<string, string>
    static requiredUseFlags():FieldsForOperation { return null!; }
    static create<T extends DataObjectBase>(
        this: (new (...a: any[]) => T) & 
            Pick<typeof DataObjectBase, keyof typeof DataObjectBase>, 
        intendedOperation: number, 
        requestBody: any
    ): T {
        let dataObject : T = null;
        const sourceData = {};
        const objFields = this.fieldNames;
        const flagCollection = this.requiredUseFlags();
        // rest of code
        return new this();
    }
}

Not with jsdoc:

   /**
     * @template {DataObjectBase} T
     * @this {new (...args: any[]) => T}
     * @returns {Promise<T>}
     */
    static create (
        intendedOperation: number, 
        requestBody: any
    ) {...}

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.

/**
 * @template  T
 * @param {T} Class
 * @returns {{
 *   method: () => InstanceType<T>
 *  } & T}
 */
// @ts-ignore
const fixPolymorphicThis = Class => Class

class Base {
    static method() {
        return new this()
    }
}
const Sub = fixPolymorphicThis(class Sub extends Base { })

let base = Base.method() // IDE should understand that base is instance of Base
let sub = Sub.method() // IDE should understand that sub is instance of Sub

@adamjeffries
Copy link

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

@berish-ceo
Copy link

berish-ceo commented Dec 3, 2022

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

@clshortfuse
Copy link

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 MixinOf I'm working on. But mixins are a bit wonkier when getting pretty deep. I'm not able to add 2 mixins of an extended class and without losing the this mapping. But for straight subclassing, the type definition will rewrite the types of all functions that return the super class to instead return the subclass. It also rewrite the this parameter for the function. I don't have to modify any functions, but do need to export the casted subclass instead of the class directly.

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 ClassOf<unknown> and it works pretty well, in my experience.

@clshortfuse
Copy link

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*

@berish-ceo
Copy link

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 Solution

Just 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 Solution

To begin with, we have defined special individual types.

PrototypeType is a function that has a prototype of the current type.

ConstructorFunctionType is a prototype that has the annotation of creation instance.

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.

ConstructorType is the base type. In T generic, the type of the instance is passed (and not the type of the class, as before). There is also a static attribute that allows you to add static properties and class methods. This type is the main one, which closes all usage scenarios.

// 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 practice

I 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 scenario

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

  ...
}

Conclusion

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

@RobertAKARobin
Copy link

RobertAKARobin commented Dec 1, 2023

@berish-ceo Great work! Any thoughts on how ConstructorType might preserve the ConstructorParameters? e.g. something like:

class Entity {
	static create<Subclass extends Entity>(
		this: ConstructorType<Subclass, typeof Entity>,
		...args: ConstructorParameters<Subclass>
	): Subclass {
		return new (this as unknown as typeof Entity)(...args) as Subclass;
	}
}

@alexeyraspopov
Copy link

Happy 8 year anniversary, my favorite TypeScript issue!

@spirobel
Copy link

Happy 8 year anniversary, my favorite TypeScript issue!

😤 this is not a laughing matter 😤

@apendua
Copy link
Contributor

apendua commented Apr 26, 2024

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:

https://www.typescriptlang.org/play/?ssl=22&ssc=1&pln=1&pc=1#code/KYDwDg9gTgLgBAYwDYEMDOa4FEB2MCWMAngDwAqcoMwOAJphAEYBWwCMAfHAN4BQccWihgoAXHDIBuXv0QQcaGFACu7aAAohI8WQCUPWQJgALfGgB0WlHAC8g4SmkCAvjIGLh+BIijBhwckoQajoGFjYYABo4AFUgkPpsPEJSMg4OdRMzcRxgAHc4TQcdfRsuGOirEoMBAV8YZSgcOFyCrLQikV0nOFdXXnw8YCgAMxQEYAkiMGAYtGGauHxacUUoQYBzWRwUAFtgVaVN3n7QSFhEVAxY+ah4mkTcAmJyadnbrj5+hHlFOGVbrYbsNzAhfP51Nwlis4AByACMsOiO324lhACkIMYcLDet1eEA

@alexeyraspopov
Copy link

There is no need to make base class generic. Similar pattern works in dataclass:

class Base {
  static create<Type extends Base>(
    this: { new (): Type },
    // ...
  ): Type {
     return new this(/* ... */);
  }
}

class User extends Base {
  
}

User.create();

@apendua
Copy link
Contributor

apendua commented Apr 27, 2024

@alexeyraspopov You're right. I was a little bit biased by my own use-case which I was trying to solve.

There is no need to make base class generic.

Probably no need if you only want to demonstrate the polymorphism of this. But in practice, the child constructors may want to accept arguments of different types. From that perspective, I think my example is actually quite useful.

@leaftail1880
Copy link

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

@RichardTMiles
Copy link

RichardTMiles commented May 28, 2024

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 intelli-sense works below for two reasons, instance is not defined in our parent class but is made available using get Instance () and set instance(...), thus allowing our library code to work as expected. Having the getters/setters fn use generic types is currently not supported in the TS language construct. Second, ChildClass explicitly defines instance, so when your editor looks for the reference, it will first see the child's explicit implementation. As it is also impossible currently to define abstract static variables, we're left manually defining it with no TS hints/errors available to notify us if the child class lacks the appropriate definition. However, defining these values is only for your editor as the parent class can and does explicitly define the static members using Object.assign(new.target, ....

Object.assign(new.target, {
    _instance: this,
    instance: this,
});
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;
    }
}

@ShaneMurphy2
Copy link

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:

https://www.typescriptlang.org/play/?ssl=22&ssc=1&pln=1&pc=1#code/KYDwDg9gTgLgBAYwDYEMDOa4FEB2MCWMAngDwAqcoMwOAJphAEYBWwCMAfHAN4BQccWihgoAXHDIBuXv0QQcaGFACu7aAAohI8WQCUPWQJgALfGgB0WlHAC8g4SmkCAvjIGLh+BIijBhwckoQajoGFjYYABo4AFUgkPpsPEJSMg4OdRMzcRxgAHc4TQcdfRsuGOirEoMBAV8YZSgcOFyCrLQikV0nOFdXXnw8YCgAMxQEYAkiMGAYtGGauHxacUUoQYBzWRwUAFtgVaVN3n7QSFhEVAxY+ah4mkTcAmJyadnbrj5+hHlFOGVbrYbsNzAhfP51Nwlis4AByACMsOiO324lhACkIMYcLDet1eEA

One downside is you can't make the constructor private or protected.

@YlguYtrid
Copy link

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:

https://www.typescriptlang.org/play/?ssl=22&ssc=1&pln=1&pc=1#code/KYDwDg9gTgLgBAYwDYEMDOa4FEB2MCWMAngDwAqcoMwOAJphAEYBWwCMAfHAN4BQccWihgoAXHDIBuXv0QQcaGFACu7aAAohI8WQCUPWQJgALfGgB0WlHAC8g4SmkCAvjIGLh+BIijBhwckoQajoGFjYYABo4AFUgkPpsPEJSMg4OdRMzcRxgAHc4TQcdfRsuGOirEoMBAV8YZSgcOFyCrLQikV0nOFdXXnw8YCgAMxQEYAkiMGAYtGGauHxacUUoQYBzWRwUAFtgVaVN3n7QSFhEVAxY+ah4mkTcAmJyadnbrj5+hHlFOGVbrYbsNzAhfP51Nwlis4AByACMsOiO324lhACkIMYcLDet1eEA

I tried copy your code to VSCode, it works well.

But I cannot understand the code follows. Const c is inferred to BaseCounter<string>, not ChildCounter<string>.

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

@trusktr
Copy link
Contributor

trusktr commented Sep 19, 2024

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"

playground

@jonathanhefner
Copy link

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 static, but lookup is actually static and should be shared between all instances of a class. Also, my code is actually only defining Base; users of my code define Derived. I do not want them to have to go through extra steps, such as defining a separate static variable and then assigning it to non-static lookup.

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

Successfully merging a pull request may close this issue.