Repository: TypeScript
Issue: Proposal: Type-side use of `instanceof` keyword as an instance-query to allow instanceof checking
Labels: Suggestion,Awaiting More Feedback

### 🔍 Search Terms

type side `instanceof` keyword used for instanceof Querying.

related issues:
#202 #31311 #42534 #50714 #55032


### ✅ Viability Checklist

- [X] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [X] This wouldn't change the runtime behavior of existing JavaScript code
- [X] This could be implemented without emitting different JS based on the types of the expressions
- [X] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [X] This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- [X] This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

### ⭐ Suggestion

A new type-side keyword `instanceof` is proposed to allow an interface to be declared as mapping to a specific class or specific constructor variable.  When type-checking for assignability of said interface, the type checker would not rely only on structural duck-typing, but also rely on the tracked class/variable from which the object was constructed.

This proposal addresses runtime errors resulting from runtime use of run-side `instanceof` that are not caught by the type checker. It may also be useful for other reasons/purposes, as shown in "Motivating Examples and discussed in "Use Cases".

Of course absence of this feature is not a bug, as the overwhelming majority of TypeScript use cases require **not** using this proposed feature.  However, there are a minority of use cases where this feature would be essential and/or useful, and the absence of this feature is a limitation.

#### Notation

The type-side syntax of `instanceof` requires as its operand either a variable representing a class constructor, e.g.:
```ts
class A {}
const a = new A() as instanceof A;  // (instanceof A & A)
```
or an instantiation statement for a generic class, e.g.:
```ts
class C<T> { constructor(t: T) {} }
const c = new C<number> as instanceof C<number>;  // // (instanceof C & C<number>)
```
The resulting type is called an "instance query type".
As shown above, an instance query type is displayed as a paraenthesized intersection with two componenets:
- The first component repreents js runtime class hierarchy defined by the js `x instanceof Y` operator, and is called the `instanceof` component.  It is represented in the type display by a constructor symbol, e.g., `instanceof A` or `instance C` above.  This is the a new ability incorporated by this proposal.
- The second component is the called the structural type component, which TypeScript already provides, e.g. `A` or `C<number>` above.

Although the instanceof query type is written as an paranthesized interseciton, it is not behave exactly the same as an intersection type. An instanceof query type is a unit type with some special rules.

#### Implementation

In order to ensure the prerequisite *"This wouldn't be a breaking change in existing TypeScript/JavaScript code"*, the behavior of code not involving the `instanceof` instance-query operator will not change.  Only the behavior of code that does involve the `instanceof` instance-query, and its resulting flow, will be affected.

##### Workaround for "new"

That means that the `new` operator will not be affected by this proposal:
```ts
class A {}
const a = new A(); // a is still of type A, not (instanceof A & A)
```
does not automicallly beome `a: instanceof A`.  A cast (as above) or a convenience function:
```ts
function createA(): instanceof A { return new A();}
```
is required.

##### Workaround for "instanceof" (the runtime usage)

Likewise the `instanceof` operator will not be affected by this proposal:
```ts
declare const a: A;
if (a instanceof A) {
   a satisfies instanceof A; // no, this is an error
}
```
A convenience function can be written instead:
```ts
declare const a: A;
function extendsInstanceOfA<T>(a: T): a is (instanceof A) & T {
    return a instanceof A;
}
if (extendsInstanceOfA(a)) {
   a satisfies instanceof A; // yes, this is NOT an error
}
```

### 📃 Motivating Examples


#### Examples 1:

This code (see [comment](https://github.com/microsoft/TypeScript/issues/202#issuecomment-2050400526)) passes type checking but throws a runtime error:
```ts
const arrayBuffer: ArrayBuffer = new Uint8Array() // ts says this is structurally ok but ...
const dataView = new DataView(arrayBuffer); // currently causes run time error
```
The `DataView` constructor requires an actual `ArrayBuffer` instance, but `Uint8Array` is not an `ArrayBuffer` instance.


With this proposal the user could write `safeDataVew` which takes an `instanceof ArrayBuffer` argument type:
```ts
    declare function safeDataView(buffer: instanceof ArrayBuffer): DataView;
```
Passed a plain `ArrayBuffer` type the TS compiler will error:
```ts
    let a: ArrayBuffer = new Uint8Array()
    safeDataView(a); // should error
// !!! error TS2345: Argument of type 'ArrayBuffer' is not assignable to parameter of type '(instanceof ArrayBuffer & ArrayBuffer)'.
```
Try to fix that error but it just moves
```ts
    let b: instanceof ArrayBuffer = new Uint8Array() // now the error is here
// !!! error TS2322: Type 'Uint8Array' is not assignable to type '(instanceof ArrayBuffer & ArrayBuffer)'.
// !!! error TS2322:   Object type Uint8Array has no constructor declared via 'instanceof' therefore is not assignable to constructor typeof ArrayBuffer
    safeDataView(b); // no error
```
Try again - still an error but the compiler tells us that Uint8Array is not an instanceof ArrayBuffer
```ts
    let c: instanceof ArrayBuffer = new Uint8Array() as instanceof Uint8Array; // still error, but better error explanation
//!!! error TS2322: Type '(instanceof Uint8Array & Uint8Array)' is not assignable to type '(instanceof ArrayBuffer & ArrayBuffer)'.
//!!! error TS2322:   new Uint8Array() instanceof ArrayBuffer is not true
    safeDataView(c); // no error
```
#### Examples 2:

Class `instanceod` heirarchy is separate from the generic type heirarchy.  They are separate, but both must be satisfied for type checking to pass.  This example shows an example where the `instanceof` comopnent passes but the generic type component fails.
```ts
        class EmptyBase {}
        class A1<T extends string|number>  extends EmptyBase{
            a: T;
            constructor(a: T) {
                super();
                this.a = a;
            }
        }
        const ANumVar = A1<number>;
        const AStrVar = A1<string>;

        const an = new A1<number>(1) as instanceof A1<number>;
        const as = new A1<string>("one") as instanceof A1<string>;

        an satisfies EmptyBase; // no error
        an satisfies instanceof A1; // no error
        an satisfies instanceof A1<number>; // no error
        an satisfies instanceof ANumVar; // no error

        as satisfies EmptyBase; // no error
        as satisfies instanceof A1; // no error
        as satisfies instanceof A1<number>; // error
//         ~~~~~~~~~
// !!! error TS1360: Type '(instanceof A1 & A1<string>)' does not satisfy the expected type '(instanceof A1 & A1<number>)'.
// !!! error TS1360:   Type 'A1<string>' is not assignable to type 'A1<number>'.
// !!! error TS1360:     Type 'string' is not assignable to type 'number'.
        as satisfies instanceof ANumVar; // error
//         ~~~~~~~~~
// !!! error TS1360: Type '(instanceof A1 & A1<string>)' does not satisfy the expected type '(instanceof A1 & A1<number>)'.
// !!! error TS1360:   Type 'A1<string>' is not assignable to type 'A1<number>'.
```

#### Examples 3-a:

Intersection of instanceof-query types with `{}` as structure type resolves to:
- `never` if the instanceof components are not in the same linear class hierarchy
- the most specific common ancestor if they are in the same linear class hierarchy

```ts
        class EmptyBase {}
        class A1  extends EmptyBase{
            a: number|string = "";
        }
        class A2  extends A1 {
            a: number = 0;
        }
        class A3  extends A2 {
            a: 0 | 1 = 0;
        }
        class B3  extends A2 {
            a: 1 | 2 = 2;
        }
        type AQ = instanceof A1 & instanceof A2;
        declare let a1: instanceof A1;
        declare let a2: instanceof A2;
        declare let a3: instanceof A3;
        declare let b3: instanceof B3;

        a1 satisfies AQ; // error (because (typeof A1).constructor < (typeof A2).constructor)
//         ~~~~~~~~~
// !!! error TS1360: Type '(instanceof A1 & A1)' does not satisfy the expected type '(instanceof A2 & (A1 & A2))'.
// !!! error TS1360:   'new A1() instanceof A2' would evaluate as 'false'
        a2 satisfies AQ; // no error
        a3 satisfies AQ; // no error
        b3 satisfies AQ; // no error

        type ANope = instanceof A3 & instanceof B3;
        a3 satisfies ANope; // error
//         ~~~~~~~~~
// !!! error TS1360: Type '(instanceof A3 & A3)' does not satisfy the expected type 'never'.
        b3 satisfies ANope; // error
//          ~~~~~~~~~
// !!! error TS1360: Type '(instanceof B3 & B3)' does not satisfy the expected type 'never'.
```

As a special consider when the intersection of an instanceof query type with a structure-only type, e.g.:

```ts
class A { a: A | undefined; constructor(a: A | undefined) { this.a = a; } }
interface Y { b:B|undefined } // no constructor!
type X =  instance A;  // (instanceof A & A)
type Z = X & Y; // (instanceof A & (A & Y)),  NOT never!
```
The reason that the result is not `never` is that , for the purpose of compiuting intersection, `Y` is internally "promoted" to an intanceof query type represented as
```ts
(instanceof Object) & Y
```
That makes sense because in js runtime, every object returned by a constructor is an instance of `Object`.

It follows that, for any instance query type `type X = instanceof SomeClass`,  the intersection `X & {}` is always `X`.


#### Examples 4:

TypeScript considers classes with completely empty publicly declarad type structure as equivalent to `any` - although they might correspond to objects with rich but hidden functionality that need to be discriminated.  This proposal would allow you to discriminate them.

```ts
        interface A1 {}
        interface A1Constructor {
            prototype: A1;
            new(): A1;
        }
        declare const A1: A1Constructor;

        interface A2  extends A1 {}
        interface A2Constructor {
            prototype: A2;
            new(): A2;
        }
        declare const A2: A2Constructor;

        declare let a1: instanceof A1;
        declare let a2: instanceof A2;
        const one = 1 as const;
        const sym = Symbol();

        ////////////////////////////////////////////////////////////////////
        // compare to rhs without instanceof -- none of these are errors, which might not be desirable.

        a1 satisfies A2; // not an error

        ({}) satisfies A2; // not an error

        one satisfies A2; // not an error

        1n satisfies A2; // not an error

        sym satisfies A2; // not an error


        ////////////////////////////////////////////////////////////////////
        // using instanceof queries these can now be discriminated

        a1 satisfies instanceof A2; // should be error
//         ~~~~~~~~~
//!!! error TS1360: Type '(instanceof A1 & A1)' does not satisfy the expected type '(instanceof A2 & A2)'.
//!!! error TS1360:   'new A1() instanceof A2' would evaluate as 'false'

        ({}) satisfies instanceof A2; // should be error
//           ~~~~~~~~~
//!!! error TS1360: Type '{}' does not satisfy the expected type '(instanceof A2 & A2)'.
//!!! error TS1360:   Object type {} has no constructor declared via 'instanceof' therefore is not assignable to constructor typeof A2

        one satisfies instanceof A2; // should be error
//          ~~~~~~~~~
//!!! error TS1360: Type 'number' does not satisfy the expected type '(instanceof A2 & A2)'.
//!!! error TS1360:   Object type Number has no constructor declared via 'instanceof' therefore is not assignable to constructor typeof A2

        1n satisfies instanceof A2; // should be error
//         ~~~~~~~~~
//!!! error TS1360: Type 'bigint' does not satisfy the expected type '(instanceof A2 & A2)'.
//!!! error TS1360:   Object type BigInt has no constructor declared via 'instanceof' therefore is not assignable to constructor typeof A2

        sym satisfies instanceof A2; // should be error
//          ~~~~~~~~~
//!!! error TS1360: Type 'typeof sym' does not satisfy the expected type '(instanceof A2 & A2)'.
//!!! error TS1360:   Object type Symbol has no constructor declared via 'instanceof' therefore is not assignable to constructor typeof A2
```


### 💻 Use Cases

As shown in the motivating examples.