John
John

Reputation: 10079

How to cast abstract class to non-abstract class?

If I have an abstract class Adapter {}, I'd like to create a function which accepts a non-abstract constructor extending this class and returns a new instance of the class.

e.g.

export abstract class Adapter {}

export function change(adapterConstructor: typeof Adapter) {
  return new adapterConstructor();
}

Unfortunately, this example doesn't work because the typescript compiler correctly complains that an abstract class cannot be instantiated.

Is it possible to limit the adapterConstructor argument to only non-abstract implementations of typeof Adapter?

i.e. something like

export abstract class Adapter {}

export function change(adapterConstructor: NonAbstract<typeof Adapter>) {
  return new adapterConstructor();
}

At the moment, my solution is to simply make the Adapter class non-abstract and have all of the methods which should be abstract throw errors.

Upvotes: 3

Views: 1885

Answers (2)

jcalz
jcalz

Reputation: 329013

Yes, you can require the change() method to take an intersection of typeof Adapter (which is not known to be newable) with a constructor signature. (EDIT for TS4.2+: the type typeof Adapter is now known to have an abstract construct signature, so you can't leave it in there. Instead, you can use Pick<typeof Adapter, keyof typeof Adapter> which keeps any static properties but discards any call/construct signature entirely).

First, I'm going to give Adapter some structure, since empty classes behave strangely in TypeScript and we don't want that to trip us up. Also I want to add a static method to Adapter so that you can convince yourself that change() will be able to call it:

export abstract class Adapter {
  prop = "adapterProp";
  static staticMethod() {
    console.log("HI THERE");
  }
}

So here's the change() signature:

export function change(
  adapterConstructor: Pick<typeof Adapter, keyof typeof Adapter> & (new () => Adapter)
) {
  adapterConstructor.staticMethod();
  return new adapterConstructor();
}

You can see that adapterConstructor is both newable and has Adapter's static method. Do note that the constructor signature is new () => Adapter, meaning it needs no arguments and produces an Adapter.

Let's make sure this behaves as you want:

const cannotConstructAbstract = new Adapter(); // error!
change(Adapter); // error! cannot assign abstract to non-abstract

You can't new the Adapter class, and you can't call change(Adapter), as desired.

Now let's make a concrete subclass:

class ChickenAdapter extends Adapter {
  cluck() {
    console.log("B'KAW!");
  }
}
    
const canConstruct = new ChickenAdapter(); // okay
const chickenAdapter = change(ChickenAdapter); // okay

You can new the ChickenAdapter class, and so you can call change(ChickenAdapter), which returns an Adapter:

chickenAdapter.prop // okay
chickenAdapter.cluck(); // error! it only returns an Adapter, not a ChickenAdapter

Note that chickenAdapter is not known to be an ChickenAdapter instance, since change() returns an Adapter. If you want change() to keep track of this, you can make it generic in the return type:

export function change<T extends Adapter>(
  adapterConstructor: typeof Adapter & (new () => T)
) {
  adapterConstructor.staticMethod();
  return new adapterConstructor();
}

And then chickenAdapter.cluck() works:

chickenAdapter.cluck(); // okay

Let's move on to some edge cases:

class CowAdapter extends Adapter {
  constructor(public color: string) {
    super();
  }

  moo() {
    console.log("MRRROOOORM!");
  }
}

const howNow = new CowAdapter("brown");
const butNeedsAnArgument = new CowAdapter(); // error! expected 1 argument
change(CowAdapter); // error!  CowAdapter needs arguments

Here, change() does not accept CowAdapter because that class requires a constructor argument, which the body of change() does not pass in. That's probably the right thing to do.

And also, let's make sure that you can't pass in something whose static side looks okay but doesn't produce an Adapter:

class NotAnAdapter {
  static staticMethod() {}
  notAnAdapter = true;
}

change(NotAnAdapter); // error!
//Property 'prop' is missing in type 'NotAnAdapter' but required in type 'Adapter'.

Also looks good.


Okay, hope that helps; good luck!

Link to code

Upvotes: 3

Fenton
Fenton

Reputation: 251012

You could go dynamic, but at the risk of allowing Adapter itself to be passed in, which kinda defeats the purpose of an abstract class...

export abstract class Adapter {}

export function change(adapterConstructor: typeof Adapter): Adapter {
    const constr: any = adapterConstructor;
    return new constr() as Adapter;
}

Otherwise, you'd need to introduce a concrete base class to use in place of Adapter... or change Adapter to be an interface that the each class would have to implement rather than using the abstract class.

Upvotes: 1

Related Questions