Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 311
Alexander O'Mara Avatar asked Apr 26 '18 02:04

Alexander O'Mara


1 Answers

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.

like image 128
Aluan Haddad Avatar answered Oct 15 '22 18:10

Aluan Haddad