Reputation: 60547
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
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