Typescript: Work with Child Class in static method

Viewed 857

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)?

1 Answers

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

Related