Använder TypeScripts InstanceType-generics med abstrakta klasser?


TypeScript 2.8 lade till en ny kärntyp InstanceType som kan användas för att få returtypen av en konstruktörsfunktion.

/**
 * 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;

Den här funktionen är ganska bra, men fallerar när man använder abstrakta klasser, som inte har en new-deklaration enligt TypeScripts typ-system.

Först trodde jag att jag kunde kringgå denna begränsning genom att skapa en liknande men mindre restriktiv typ (genom att ta bort extends new (...args: any[]) => any-skyddet):

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

Men det faller också isär när det passeras en abstrakt klass, eftersom det inte kan härleda returtypen och standardinställs till any. Här är ett exempel som använder en mock-DOM som exempel, med försök till typomvandling.

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

Finns det något sätt jag kan tycka att en variabel är en instans av den konstruktor som passerats, om den konstruktor är en abstrakt klass?

OBS: Jag försöker undvika att använda instanceof eftersom JavaScripts instanceof är mycket problematiskt (2 olika versioner av samma modul har olika konstruktörsinstanser).

Svar #1

Du kan fråga typen av prototype för en abstrakt class för att få typen av dess instanser. Detta kräver inte att typen har en new-signatur, endast att den har en prototype-egenskap. Abstrakta klasser har inte en new-signatur men de har en prototype-egenskap.

Här är hur det ser ut

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

Uttrycket C['P'] i typ-position kallas en indexerad åtkomsttyp. Det är typen för värdet av egenskapen med namnet P i typen C.

Relaterade artiklar