Description
[A search yields this addresses #7271. However, I found that out only after it was written, but still decided to post it separately, as I felt it wouldn't gain enough exposure there]
Since TS classes are purely structural, there is no current way to precisely model the instanceof
runtime expression, which is the idiomatic way that instances of classes are tested at runtime:
class A {
prop: number;
}
class B {
prop: number;
}
function giveMeA(x: A) {
if (!(x instanceof A))
throw new TypeError("This is not A!");
}
giveMeA(new A()); // Compiles and works
giveMeA(new B()); // Compiles but errors in runtime: "This is not A!"
Proposed solution
instanceof T
, where T
is a class or possibly a function type, is a strict subtype of T
that only includes the nominal reference to T
, or any one of its nominal subtypes, and excludes all other structurally compatible types.
It is used as follows:
class A {
prop: number;
}
class B {
prop: number;
}
function giveMeA(x: instanceof A) {
if (!(x instanceof A))
throw new TypeError("This is not A!");
}
giveMeA(new A()); // Compiles and works
giveMeA(new B()); // Compilation error: "[instanceof] B is not assignable to type 'instanceof A'"
Implicit narrowing through assignment and flow analysis
In most cases, there wouldn't be a need to explicitly state let x: instanceof A
as it would be implicitly inferred through code analysis:
let x = new A(); // Type of 'x' is 'instanceof A'
x = new B(); // Type of 'x' has now been widened back to 'A'
// or alternatively implicitly changed to 'instanceof B'!
The instanceof
runtime operator will now be used as a guard that will also cause the type to be narrowed:
declare var x: A; // Type of 'x' is 'A' - it cannot be narrowed as not enough info is available
declare var y: B; // Type of 'y' is 'B'
if (x instanceof A) {
// Type of 'x' is now narrowed to `instanceof A`
x = y; // Type of 'x' is now widened back to 'A'
}
else if (x instanceof B) {
// Type is now narrowed to `instanceof B`
x = y; // Type of 'x' is now widened to 'A' (not 'B')
}
Application on plain constructor functions
The same behavior may also be given to regular functions that are used as constructors with the new
keyword (I'm using some proposed syntax here for illustration):
function A(this: { prop: number }) {
this.prop: = 1;
}
function B(this: { prop: number }) {
this.prop: = 2;
}
function giveMeA(x: instanceof A) {
if (!(x instanceof A))
throw new TypeError("This is not A!");
}
giveMeA(new A()); // Compiles and works
giveMeA(new B()); // Compilation error: 'instanceof B' is not assignable to 'instanceof A'
Remarks
The more conceptual way to look at this is that it takes nominal subtyping and simply views it as a specialized subdivision of structural subtyping (similarly to the way, say, string literal types are a specialization of string types), and allows both to live together in a backwards compatible way, without requiring explicitly stating a class as particularly being 'nominal' or 'structural'. It doesn't require any compiler switches and most of the time would be effective implicitly through assignment and flow analysis.
I have no idea of the difficulty of implementing this, especially the analysis part, but I believe it might be possible, considering flow analysis has already been proven viable and integrated to the next release. I'm aware this may not turn out to be the 'ultimate' solution that would eventually chosen, but I thought this was interesting enough to share.