Alexander O'Mara
Alexander O'Mara

Reputation: 60547

Using TypeScript's InstanceType generics with abstract classes?

TypeScript 2.8 added a new core type InstanceType which can be used to get the return type of a constructor function.

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

This feature is pretty nice, but falls apart when using abstract classes, which don't have a new declaration according to TypeScript's type system.

At first, I thought I could get around this limitation by creating a similar but less-restrictive type (removing the extends new (...args: any[]) => any guard):

export type InstanceofType<T> = T extends new(...args: any[]) => infer R ? R : any;

But it too falls apart when passed an abstract class, as it cannot infer the return type and defaults to any. Here's an example using a mock DOM as an example, with attempted type casting.

abstract class DOMNode extends Object {
    public static readonly TYPE: string;
    constructor() { super(); }
    public get type() {
        return (this.constructor as typeof DOMNode).TYPE;
    }
}

class DOMText extends DOMNode {
    public static readonly TYPE = 'text';
    constructor() { super(); }
}

abstract class DOMElement extends DOMNode {
    public static readonly TYPE = 'text';
    public static readonly TAGNAME: string;
    constructor() { super(); }
    public get tagname() {
        return (this.constructor as typeof DOMElement).TAGNAME;
    }
}

class DOMElementDiv extends DOMElement {
    public static readonly TAGNAME = 'div';
    constructor() { super(); }
}

class DOMElementCanvas extends DOMElement {
    public static readonly TAGNAME = 'canvas';
    constructor() { super(); }
}

// Create a collection, which also discards specific types.
const nodes = [
    new DOMElementCanvas(),
    new DOMText(),
    new DOMElementDiv(),
    new DOMText()
];

function castNode<C extends typeof DOMNode>(instance: DOMNode, Constructor: C): InstanceofType<C> | null {
    if (instance.type !== Constructor.TYPE) {
        return null;
    }
    return instance as InstanceofType<C>;
}

// Attempt to cast the first one to an element or null.
// This gets a type of any:
const element = castNode(nodes[0], DOMElement);
console.log(element);

Is there any way I can cast a variable to being an instance of the constructor that is passed, if that constructor is an abstract class?

NOTE: I'm trying to avoid using instanceof because JavaScript's instaceof is very problematic (2 different versions of the same module have different constructor instances).

Upvotes: 5

Views: 6697

Answers (1)

Aluan Haddad
Aluan Haddad

Reputation: 31833

You can query type of the prototype of an abstract class to obtain the type of its instances. This does not require that the type have a new signature only that it has a prototype property. Abstract classes do not have a new signature but they do have a prototype property.

Here is what it looks like

function castNode<C extends typeof DOMNode>(
  instance: DOMNode,
  Constructor: C
): C['prototype'] | null {
  if (instance.type !== Constructor.TYPE) {
    return null;
  }
  return instance;
}

The expression C['P'] in type position is called an indexed access type. It is the type of the value of the property named P in the type C.

Upvotes: 7

Related Questions