Skip to content

Suggestion: the 'instanceof' type modifier for class and function types #8316

Closed
@malibuzios

Description

@malibuzios

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    RevisitAn issue worth coming back toSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions