-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Suggestion: the 'instanceof' type modifier for class and function types #8316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Some additional remarks after thinking about this further: (1) A contextual narrowing can be made for parameter types of functions or methods that internally narrow the type to its function giveMeA(x: A) {
if (!(x instanceof A))
throw new TypeError("This is not A!");
x.prop = 1234;
} With flow analysis, the compiler can realize that the statement function giveMeA(x: instanceof A) { (2) A global compiler switch like (3) I'm aware that questioning prior design decisions is a somewhat of a 'taboo' subject here, but I must mention that if all classes were nominal in the first place, it would have been much easier to simply have a counterpart let x: A;
let y: B;
x = y; // This wouldn't work if classes were nominal
let z: structureof A;
z = y; // OK.. Say a function wanted the exact class instance, but the user only had a structurally compatible one, they could simply use a cast. Which would be seen as a contravariant one: declare function giveMeA(a: A);
let x = { prop: 1234 };
givaMeA(x); // Error: type '{ prop: number }' is not assignable to 'A'
givaMeA(<A> x); // Works, as the cast is considered contravariant there is no need to use
// something unsafe like '<any>' In the 3+ years I have been using TypeScript I believe there were very few occasions where I used a structural assignment between classes or between interfaces and classes. For the few cases I might have done that, I feel it would have been fine for me to use one of these solutions. |
Also essentially a dupe of #8168 as well, related to #1719 and specifically discussion in #1719 (comment). Nominal typings is being dealt with under #202. |
Thank you for tracking down similar issues and related comments, this is valuable information for the readers who may be interested in this. I don't feel that any of these would be a suitable place to present this particular approach. The purpose of this is not particularly about introducing nominal typing. It is about creating an implicit nominal 'awareness' for classes (only) and prototype chains, the same way that string literal types are a form of a an awareness for the specialization of string type. Just make sure that the discussion is not diverted to trivial matters like whether this is a 'duplicate' or not: I would not have spent the 5-6 hours it took to investigate, write and edit this if it was 'buried' in a closed or old issue that very few people are exposed to. |
I think a good way to look at this is "use-site" nominal typing, as opposed to "declaration-site" nominal typing. In other words, instead of deciding on whether a class itself is nominal or not when authoring it, consumers need to assert that they really only mean to use the nominal class. This is a little more cumbersome, but it does definitely leave room open for flexibility when using your classes. |
Hi, I wrote these comments several days ago but felt I haven't managed to thoroughly research and figure them out completely. I've decided to post them anyway: (1): I must admit that at first I actually felt this was somewhat superfluous (despite the fact that I proposed it myself..) because it seems like having all classes to become nominal looks like a dramatically simpler way to achieve an equivalent or better level of type safety. However, now I realize there is more to this: When declare function iWantInstanceOfA(a: instanceof A);
function iOnlyCareAboutTheStructureOfA(a: A) {
iWantInstanceOfA(a); // Error: type 'A' is not assignable to 'instanceof A'
// which is solved either by a guard:
if (a instanceof A)
iWantInstanceOfA(a); // OK
// or by a less safe, contravariant cast:
iWantInstanceOfA(<instanceof A> a) // OK
} So in a way having "less strict" typing for classes in general, requires more 'work' for the programmer to try to satisfy a type that is 'stricter than usual'. That's interesting.. (2): Rather subtle point: declare let x: A;
if (x instanceof A) {
// 'x' gets type 'instanceof A'
}
else if (x instanceof B) {
// 'x' gets type 'instanceof B'
}
else {
// But what happens here? does it resort back to 'A'?
} For maximum percision, the else {
let y = <instanceof A> x; // Error.. type 'A - (instanceof A | instanceof B)' cannot
// be directly cast to type 'instanceof A'
} (3): Another issue is on how to get more precise and useful flow analysis, and more correctly model the set of possible instances and the effects of the function f() {
if (x instanceof A || x instanceof B) {
return;
}
// say 'x' gets the precise type 'A - (instanceof A | instanceof B)':
if (x instanceof A) {
// 'x' should ideally get type 'nothing', but does the subtraction type really help here?
}
else if (x instanceof B) {
// same here..
}
} (4): My next point here was supposed to be an attempt to try to improve the (very rudimentary and rather inaccurate) heuristic I tried to suggest for the 'back propagation' of flow analysis from the function's body to the signature itself - it essentially tries to analyze the intention of the programmer on whether they want the structural or the more specific nominal type. It turned out this was more difficult than I initially thought. However a good solution for this may turn out to be useful in other areas. I'm still working on it. |
As noted in #8503, we will be fixing the instanceof checks to work nominally for classes (as they should). This work is tracked by #7271. Fixing #7271 should eliminate the immediate need for this proposal. The general issue of how classes are compared, and whether they should be nominal or structural, is something that we have got over the years. We believe that adding an explicit tagging support for classes/types declaration to switch them to nominal treatment is the way this request would be best met, rather than doing it at use sites. This is best tracked by: #202. |
Possibly revisit this decision when investigating #202. |
[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:Proposed solution
instanceof T
, whereT
is a class or possibly a function type, is a strict subtype ofT
that only includes the nominal reference toT
, or any one of its nominal subtypes, and excludes all other structurally compatible types.It is used as follows:
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:The
instanceof
runtime operator will now be used as a guard that will also cause the type to be narrowed: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):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.
The text was updated successfully, but these errors were encountered: