1

I have 2 immutable classes which extends an abstract class which looks as follows:

abstract class Enemy {
    abstract readonly attackPower: number;

    constructor(
        public readonly health: number
    ) {
    }

    abstract hit(): Enemy;

}

class Boss extends Enemy {
    attackPower = 100;
    
    hit(): Boss {
        return new Boss(this.health - 1);
    }
}

class NormalEnemy extends Enemy {
    attackPower = 5;

    hit(): NormalEnemy {
        return new NormalEnemy(this.health - 1);
    }
}

Note these are immutable classes, so all properties are read-only.

As you can see the "hit" method is being repeated.

Ideally I would like to move to logic to the Enemy abstract class to something like:

abstract class Enemy {
    abstract readonly attackPower: number;

    constructor(
        public readonly health: number
    ) {
    }

    hit() {
         return new self(health - 1); // not valid TypeScript code
    }
}


class Boss extends Enemy {
    attackPower = 100;
}

class NormalEnemy extends Enemy {
    attackPower = 5;
}

However this is of course not valid.

Anyway to solve this issue?

2
  • What if you want a new enemy to take more damage than just 1? Commented Sep 2, 2022 at 16:48
  • @kelly For this example, we don't need to consider that case. But I imagine you could just add an argument to hit, such as hit(attack: number) Commented Sep 2, 2022 at 23:41

1 Answer 1

2

You may use this.constructor in a similar manner:

abstract class Enemy<ChildClass extends Enemy<any>> {
    abstract readonly attackPower: number;

    constructor(
        public readonly health: number
    ) {
    }

    hit(): ChildClass {
         return new (this.constructor as { new (...args: any[]): ChildClass })(this.health - 1);
    }
}

I have added a generic to Enemy so that hit returns the correct type, and also so that we can cast this.constructor to the correct type (creates instance of ChildClass).

This is what the child classes look like now:

class Boss extends Enemy<Boss> {
    attackPower = 100;
}

class NormalEnemy extends Enemy<NormalEnemy> {
    attackPower = 5;
}

And if we test it, it works:

let boss = new Boss(10);

boss = boss.hit();

console.log(boss, boss.health);
/*
Boss: {
  "health": 9,
  "attackPower": 100
},  9 
*/

Playground


If you really want something similar to "self" in other languages, you could store new.target and use that in the hit method:

abstract class Enemy<ChildClass extends Enemy<any>> {
    abstract readonly attackPower: number;

    private readonly self;

    constructor(
        public readonly health: number
    ) {
        this.self = new.target;
    }

    hit(): ChildClass {
         return new (this.self as { new(...args: any[]): ChildClass })(this.health - 1);
    }
}

new.target reference

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.