Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does `type Constructor<T> = Function & { prototype: T }` apply to Abstract constructor types in TypeScript?

There is this Q&A about Abstract constructor types in TypeScript, which is a question I would like to know the answer to:

Abstract constructor type in TypeScript

Unfortunately, the accepted answer is little more that an unexplained one-liner: type Constructor<T> = Function & { prototype: T }

It appears that this answer is good enough for the asker, it is the accepted answer, but I'm unable to understand how the answer applies to the question.

I've tried asking in the comments, and I've tried asking someone in chat to explain the answer, but to no avail. Also, I recognize that this is only barely a different question from that one, but this is still a unique question about code.

How can I use a type of Function & { prototype: T } to emulate an abstract class of known constructor type?

like image 871
Seph Reed Avatar asked Sep 15 '18 23:09

Seph Reed


1 Answers

In JavaScript, all functions have a special property called prototype. If you use a function as a constructor (by calling it with new), the constructed instance will have that as its prototype. This is how classes worked in JavaScript before ES2015:

// ES5-flavored class 
function Foo() { } // a normal function 
Foo.prototype.prop = 0; // adding a property to Foo.prototype
var f = new Foo(); // using Foo as a constructor
f.prop; // the instance inherits the added property

ES2015 introduced the class syntax, which behaves the same way under the hood... and so constructor objects still have a prototype property from which constructed instances inherit.

// ES2015-flavored class
class Foo { } // explicitly a class
Foo.prototype.prop = 0; // adding a property to Foo.prototype
const f = new Foo(); // using Foo as a constructor
f.prop; // the instance inherits the added property

TypeScript models this by declaring that the Function interface, which all functions implement, has a prototype property of type any. Furthermore, when the class syntax is used, the constructor is still seen as a Function, and the prototype property of the constructor is narrowed from any to the same type as the constructed class instance:

// TypeScript code
class Foo { 
  prop!: number;  // Foo has a prop
} 
Foo.prototype; // inspects as type Foo
Foo.prototype.prop = 0; // so this works
const f = new Foo(); // inspects as type Foo
f.prop; // inspects as type number

Even abstract classes in TypeScript have a prototype property whose type is the same as the instance type. So while you can't call new on an abstract class constructor, or match it to a newable type like new() => any, you can still talk about its prototype:

// more TS code
abstract class Bar {
  abstract prop: number;
}
Bar.prototype; // inspects as type Bar
Bar.prototype.prop = 0; // so this works
const b = new Bar(); // whoops can't do this though

All this means that in TypeScript a possibly-abstract class constructor is (a subtype of) a Function whose prototype property is the instance type for the constructor. Thus, you can say that

type PossiblyAbstractConstructor<T> = Function & {prototype: T};

using the intersection operator to combine Function and "object with a prototype property of type T"...

or similarly

interface PossiblyAbstractConstructor<T> extends Function {
  prototype: T;
}

using interface extension to achieve the same effect...

and you know that the following is valid:

const fooConstructor: PossiblyAbstractConstructor<Foo> = Foo;
const barConstructor: PossiblyAbstractConstructor<Bar> = Bar;

That should explain how the original question's answer is applicable. Hope that helps. Good luck!

like image 187
jcalz Avatar answered Oct 28 '22 03:10

jcalz