2

In TypeScript (I used the Playground, version 4.13) , when I inherit from a class, this inside a static method of the parent class seems to refer to the inheriting class:

class Parent{
    static ID = 0
    public id: number

    static create(){
        return new this(this.ID)
    }

    constructor(id: number){
        this.id = id
    }
}

class Child extends Parent{
    static ID = 1
}

let child = Child.create()
console.log(child.id)  // 1

However, I am having problems when I want to define some behavior depending on the type of the child class:

class Parent{
    static create(data: object){
        let instance = new this
        for (let [key, value] of Object.entries(data)){
            this[key] = value
        }
        return instance
    }
}

class Child extends Parent{
    id: number | null = null
}

let child = Child.create({id: 1})
console.log(child.id)

This gives me

Element implicitly has an 'any' type because expression of type 'string | number | symbol' can't be used to index type 'typeof Parent'. No index signature with a parameter of type 'string' was found on type 'typeof Parent'.

I tried to go round this problem, by typecasting the key as a key of the child class:

class Parent{
    static create(data: object){
        let instance = new this
        for (let [key, value] of Object.entries(data)){
            this[key as keyof this] = value
        }
        return instance
    }
}

class Child extends Parent{
    id: number | null = null
}

let child = Child.create({id: 1})
console.log(child.id)

But this is forbidden. I get

A 'this' type is available only in a non-static member of a class or interface

Also, I get (in all scenarios)

Property 'id' does not exist on type 'Parent'.

How can I solve my problem - dynamically populate the properties of the child class from an object (which I receive from an API in my real-world scenario)?

Jonathan Scholbach
  • 4,925
  • 3
  • 23
  • 44
  • I suggest you take a look a the js methods: bind(), call(), apply() and how the "this" keyword works in JS. – GBra 4.669 Jan 13 '21 at 23:42

1 Answers1

4

You can accomplish this by specifying a this parameter corresponding to the class itself. In a static method, this refers to the class itself.

static create<T extends Parent>(this: new (...args: any[]) => T, data: object) {...} 

What's going on here is that we are saying that the this type, which will refer to the object containing the method, in this case whatever class object that create is called on, can return a subtype of the instance type of the class. This is accomplished via the type parameter T, and the ascription that the class object will have a construct signature that returns a T, thus capturing the instance type of any derived class.

Here's the full working code:

class Parent {
    static create<T extends Parent>(this: new (...args: any[]) => T, data: object) {
        let instance = new this;
        for (let [key, value] of Object.entries(data)) {
            instance[key as keyof T] = value;
        }
        return instance;
    }
}

class Child extends Parent {
    id: number | null = null;
}

let child = Child.create({ id: 1 });
console.log(child.id);

Playground Link

Since we have captured the derived instance type via T, we can refine the create method further to improve type safety by adjusting create as follows:

static create<T extends Parent>(this: new (...args: any[]) => T, data: Partial<T>) {...}

This prevents us from passing, and thus assigning arbitrary properties to objects created by the create method while providing intellisense.

The full code:

class Parent {
    static create<T extends Parent>(this: new (...args: any[]) => T, data: Partial<T>) {
        let instance = new this;
        for (let [key, value] of Object.entries(data)) {
            const k = key as keyof T;
            instance[k] = value as T[typeof k];
        }
        return instance;
    }
}

class Child extends Parent {
    id: number | null = null;
}

let child = Child.create({ id: 1 });
console.log(child.id);

Playground Link

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
  • Sorry for this follow-up question: In my original problem, I also have a static variable on `Parent`. When I access this class member now in `create`, I get _Property does not exist on type 'new (...args: any[]) => T'_ I don't know how to fix this. I would expect the property to exist on T, since T extends Parent. But it does not work ... – Jonathan Scholbach Jan 14 '21 at 12:46
  • ... The playground: https://www.typescriptlang.org/play?#code/MYGwhgzhAEAKYCcCmA7ALtA3gKGn6EaYaAlsNALICSActALzQAM2u+hxZ0wyxSAPABVoSAB5pUAExjxk6AHwAKNAAsSEAFzQUSAO7RFAOmOIA5puhgUATwDaAXQCUDedEEAaaJOJgtAewAjACskYDRHLWEcfBjoECQMEhQOFGAkBm09aFV1NljoADM-BAN4jFsAayRrTwA3MBAAVyR7aD8C6AB5YNC0Q1Q0BBIkCEVvIkdnaPzYkg7Feqb01xyIQ2oaR2mZmaSUtMrqyxgq63a3VsZF5ryd6ABfW5jHneQ0RoQUaD2iVKRbx6PbCgSAwADCahAkhE4ikMkQAywtxIki0KEaAFsAkgSgAfbSNEAgDLoonYIFlbiQ6GMCEkKGGHhIPiKTDfVHQABMD0cwL8yT88UMID8pkUwGphhRjiAA – Jonathan Scholbach Jan 14 '21 at 12:46
  • You have to do something like `this: {[P in keyof typeof Parent]: typeof Parent[P]} & (new (...args: any[]) => T)`. A word of advice. You seem to keep getting the static and instance side of the class confused. Static members are never available via the type produced by constructing the class. Personally, I think using a lot of static members is an anti pattern. It's much simpler and more idiomatic to just export factory functions and use closures – Aluan Haddad Jan 14 '21 at 15:01
  • Thanks for the advice. I am coming from Python, where I am used to work with class members which can be addressed by both the class and its instances. So, I guess your idea might be right - thanks for pointing me into the right direction. – Jonathan Scholbach Jan 14 '21 at 15:14