sam256
sam256

Reputation: 1421

Proper return type for an abstract function that copies a subclass

I have an abstract class that has a public instance method copy() used for copying itself (to avoid mutating data). The logic for actually making the copy(), however, lives in a protected abstract makeACopy() method that must be implemented by each subclass to ensure it copies itself with the correct constructor.

The problem I'm having is that I can't get typescript to infer the return type of the public copy() method to be the subclass from which it's actually called. Let me illustrate:

abstract class BaseClass {
    protected data: number
    constructor(data?: number) {
        this.data = data ? data : 0
    }
    protected abstract makeACopy() //???

    public copy() {
        return this.makeACopy()
    }
}

class MyClass extends BaseClass {
    constructor(data?: number){
        super(data)
    }
    protected makeACopy(){
        const newClass = new MyClass()
        newClass.data = this.data
        return newClass
    }
}

let x = new MyClass(4)
let y = x.copy()
//  ^-- I want this to be inferred as MyClass, not BaseClass

I've tried typing the return type as BaseClass in the protected abstract declaration, but that obviously makes it too generic. I've tried protected abstract makeACopy<T extends BaseClass>():T, but that errors (and also doesn't really make sense to me anyway).

This closest I could get is making the return type on the abstract declaration this, following a similar question here. But that leads to an error on the subclass declaration that "this" could be instantiated with a different subtype of MyClass, which, in fairness to TypeScript, it could; it's just not what I'm doing here.

I feel like I'm missing something pretty obvious here, but I can't figure out what it is.

Here's the playground.

Update: I now see that it works if I defined protected abstract makeACopy(): this and then make the subclass method:

protected makeACopy(){
    const newClass = new MyClass()
    newClass.data = this.data
    return newClass as this
}

I don't really love the cast though.

Upvotes: 0

Views: 208

Answers (2)

csisy
csisy

Reputation: 510

Since TS 2.3, you can use the ThisType helper on the parent, and the concrete type on the child.

interface IFoo {
  get value(): number;

  clone(): ThisType<this>;
}

class Foo implements IFoo {
  get value(): number {
    return 42;
  }

  clone(): Foo {
    return new Foo();
  }
}

const f = new Foo();
const f2 = f.clone();
console.log(f2.value);

Note from the docs:

Note that the noImplicitThis flag must be enabled to use this utility.

Upvotes: 1

Pritam Kadam
Pritam Kadam

Reputation: 2527

Check this

abstract class BaseClass<T extends BaseClass<T>> {
  protected data: number
  constructor(data?: number) {
    this.data = data ? data : 0
  }
  protected abstract makeACopy(): T

  public copy() {
    return this.makeACopy()
  }
}

class MyClass extends BaseClass<MyClass> {
  constructor(data?: number) {
    super(data)
  }
  protected makeACopy() {
    const newClass = new MyClass()
    newClass.data = this.data
    return newClass
  }
}

let x = new MyClass(4)
let y = x.copy() /// inferred type is MyClass

Upvotes: 1

Related Questions