microsoft / microsoft/TypeScript
Proposal: Type-side use of `instanceof` keyword as an instance-query to allow instanceof checking
Nessuno ha ancora preso questa issue.
- Lingua principale
- Go
- Stelle
- 111k
- Fork
- 14.4k
- Merge medio
- 1g 19h
- PR unite (30g)
- 117
Descrizione
🔍 Search Terms
type side instanceof keyword used for instanceof Querying.
related issues:
#202 #31311 #42534 #50714 #55032
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- 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.:
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 Yoperator, and is called theinstanceofcomponent. It is represented in the type display by a constructor symbol, e.g.,instanceof Aorinstance Cabove. 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.
AorC<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:
neverif 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.
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Non sono indicati file sorgente, test o punti di ingresso. Inizia esaminando la proposta e le issue correlate #202, #31311, #42534, #50714 e #55032 per comprendere le questioni di progettazione irrisolte; per considerare il lavoro completato sarebbero necessari un design concordato, un'implementazione e una copertura di regressione per i casi di controllo dei tipi descritti.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- javascript, typescript
- Ambito
- compilers
- Tipo di issue
- Funzionalità
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Ferma
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 20/100