microsoft / microsoft/TypeScript

Proposal: Type-side use of `instanceof` keyword as an instance-query to allow instanceof checking

Ouverte
#58,181 7 commentaires 9 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Awaiting More Feedback Suggestion
Langage dominant
Go
Étoiles
111k
Forks
14.4k
Merge moyen
1 j 19 h
PR mergées (30 j)
117

Description

🔍 Search Terms

type side instanceof keyword used for instanceof Querying.

related issues:
#202 #31311 #42534 #50714 #55032

✅ Viability Checklist
⭐ 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.:

class A {}
const a = new A() as instanceof A;  // (instanceof A & A)

or an instantiation statement for a generic class, e.g.:

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:

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:

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:

declare const a: A;
if (a instanceof A) {
   a satisfies instanceof A; // no, this is an error
}

A convenience function can be written instead:

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) passes type checking but throws a runtime error:

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:

    declare function safeDataView(buffer: instanceof ArrayBuffer): DataView;

Passed a plain ArrayBuffer type the TS compiler will error:

    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

    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

    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.

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

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

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

        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.

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Aucun fichier source, test ou point d’entrée n’est nommé. Commencez par examiner la proposition et les issues associées #202, #31311, #42534, #50714 et #55032 afin de comprendre les questions de conception non résolues ; considérer le travail comme terminé nécessiterait une conception approuvée, une implémentation et une couverture de régression pour les cas de vérification de types décrits.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript, typescript
Domaine
compilers
Type d'issue
Fonctionnalité
Difficulté
5/5
Temps estimé
Plus d'une semaine
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
20/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.