microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.86k stars 12.36k forks source link

Support user-defined type guard functions #1007

Closed RyanCavanaugh closed 9 years ago

RyanCavanaugh commented 9 years ago

We currently only support some baked-in forms of type checking for type guards -- typeof and instanceof. In many cases, users will have their own functions that can provide run-time type information.

Proposal: a new syntax, valid only in return type annotations, of the form x is T where x is a declared parameter in the signature, or this, and T is any type. This type is actually considered as boolean, but "lights up" type guards. Examples:

function isCat(a: Animal): a is Cat {
  return a.name === 'kitty';
}

var x: Animal;
if(isCat(x)) {
  x.meow(); // OK, x is Cat in this block
}

class Node {
  isLeafNode(): this is LeafNode { throw new Error('abstract'); }
}
class ParentNode extends Node {
  isLeafNode(): this is LeafNode { return false; }
}
class LeafNode extends Node {
  isLeafNode(): this is LeafNode { return true; }
}
var someNode: LeafNode|ParentNode;
if(someNode.isLeafNode()) {
  // someNode: LeafNode in this block
}

The forms if(userCheck([other args,] expr [, other args])) { and if(expr.userCheck([any args])) would apply the type guard to expr the same way that expr instanceof t and typeof expr === 'literal' do today.

mintern commented 7 years ago

@shelby3 I'm not going to say any more than this: from an outsider perspective, your tone is consistently aggressive. Your comments seem (to me!) to be dripping with anger. I think part of it is words like "son", which many people consider to be demeaning. Direct commands are probably another component of the perceived anger. Meanwhile, I feel like everyone else has been patiently trying to describe TypeScript's motivations, design decisions, and philosophy.

Anger and aggression in a discussion like this will convince no one.

I think you're probably right when you say that TypeScript has design goals that don't match your concerns. Maybe you could fork it and create a new language that linearizes interfaces in the way you describe? That sounds like it could be interesting, although it would be a rather different language from TypeScript, and that's OK.

shelby3 commented 7 years ago

@spion wrote:

You started to veer off the technical rail by discussing age and testosterone

That was a response to equally abusive precedent provocateur verbiage. It helps to get your chronology correct if you want to understand who is the instigator. If you continue this political grandstanding, you will go on my block also. Stay on the technical topic, otherwise this discussion ends. (which is probably what you all are trying to accomplish any way)

Hell, you even sliced my comment at the "you are wrong" point and proceeded to ignore the rest of it, pretending it doesn't exist.

When you start with an abusive false accusation on a person what do you expect? If someone is wrong, you should be able to quote what they have written that is wrong.

I am willing to discuss the design goals of TypeScript in this thread, but you don't achieve that by saying I am wrong when I have never made any strong claim about those design goals. I have clearly communicated to you that those design goals still seem somewhat conflicting and ambiguous to me.

You are inconsistent

Another false accusation without any quote to prove this. Where have I repeatedly been inconsistent? You are fabricating an evil monster in your mind because your subconscious has decided I am the enemy of the good of TypeScript and that I refuse to understand, etc.. I know very well how this spooked packdog hindbrain works.

Oh somebody rained on your whizbang feature and has stated emphatically they think the feature is unsound and will deride TypeScript for promoting such unsoundness. And so you need to protect your tribe. Good boy. Thanks for barking loudly to warn all of us.

Fact is I have stated that I am willing to accept that if TypeScript's community and goals diverge from mine, then I have no desire to force my incongruity on them. I thought we were going to have a technical discussion, to try to illuminate if our divergence is really significant or not. But instead all this hindbrain packdog crap is making that possibility remote.

I suggest you use TypeScript for at a couple of weeks before commenting further on it.

I suggest you program for 34 years, then I'll grant you have the experience to determine whether I think I have enough relevant experience. You've gone ballistic on my person, instead of focusing on technical discussion.


@mintern wrote:

I think part of it is words like "son", which many people consider to be demeaning

I considered the continuous precedent provocateur snide verbiage accusing me of basing my comments on feelings as deserving of the response they have received.

Btw, "son" is more of a "sigh", as in "here we go again, always the same immature crap on the Internet where people would rather focus on personality than on substance and technical production".

I am not angry. I have better things to do with my time. You all are wasting time and escalating baggage.

Back on technicals exclusively or end the discussion? What is your choice kids (level of maturity/experience is evident by the lack of the ability to move on)?

your tone is consistently aggressive

Maybe you prefer feminine? My tone is matter-of-fact and masculine in the sense of stoic bluntness. I have not gone out-of-way to be disrespectful. I have offered everyone the same level of respect they show to me. And it is focused on technical discussion and trying to understand unambiguously the goals and philosophy of TypeScript.

Disagreement can alter perception.

Direct commands are probably another component of the perceived anger.

Huh? Where have I issued commands on anyone here?

Meanwhile, I feel like everyone else has been patiently trying to describe TypeScript's motivations, design decisions, and philosophy.

And I have been patiently discussing, except for the attempts to attack me starting with the first reply to me in this thread by @kitsonk that didn't discuss any technicals and instead was calling me out personally as wasting my time and effort. He then clarified that his intention was concern for me, and I thanked him for clarifying. Then @RyanCavanaugh accused me of basing my concern on "feelings" and I strongly rebuked this, because it was an afront to my desire to apply objectivity. Then @aluanhaddad went ballastic on snide comment stating I wasn't sincere about not employing my feelings. And then he piled on after I replied in kind pointing out to him that perhaps he doesn't yet have the maturity to understand how to contain his ego and tribal hindbrain, and the other packdogs have joined in the defense of the tribe. This social pattern is so common in groups, I can always see it coming. I knew right from the moment that @kitsonk posted, it had likely begun but I tried to hope "maybe not this time". But of course, it is always the same. No community will ever allow independent thought from the outside.

shelby3 commented 7 years ago

@mintern wrote:

Maybe you could fork it and create a new language that linearizes interfaces in the way you describe?

Please re-read my comments more carefully:

@shelby3 wrote:

And since interfaces have no implementation, they don't need linearization (they can be placed in the prototype chain in any random order).

shelby3 commented 7 years ago

@spoin wrote:

Its not a type system that ensures correctness. Its a type system that works with the user to ensure it.

Then afaik it is not a type system (rather some form of heuristic with probably eventual degradation into random noise). A type system by definition is:

In programming languages, a type system is a collection of rules that assign a property called type to various constructs a computer program consists of, such as variables, expressions, functions or modules.[1] The main purpose of a type system is to reduce possibilities for bugs in computer programs[2] by defining interfaces between different parts of a computer program, and _then checking that the parts have been connected in a consistent way_.

A type system associates a type with each computed value and, by examining the flow of these values, attempts to ensure or prove that no type errors can occur.

So if you introduce human error in between the declarations and the checking of their consistent interaction, then you have defeated typing.

Every value that enters the typing system has to have a correct declaration. Normally this is enforced by the compiler because it won't allow an assignment of an incompatible value to an instance of a type. But if you allow a human to tell the compiler whether a value is correct, then this enforcement is vacated.

It is possible to interopt with JavaScript's dynamically typed constructs by typing these as any in the sound type system and never allow the assignment of a value from an any to enter into the sound typing system. You could still do operations on any types knowing these operations are not sound (because they aren't even checked by the compiler), without violating the soundness of the type system.

I believe JavaScript can be typed soundly at compile-time for the portions in the compiler's type system (e.g. N4JS probably already does this), but this won't insure runtime soundness, because the compiler can't control all of the runtime. Nevetheless it will insure that the compiler's type system is internally consistent and thus reliable in that respect (at that orthogonal layer). If you violate this, then you will most likely (because of the pendulum example in chaos theory) end up eventually with a type system that is incredibly unreliable and eventually useless.

Escaping out of the type system means accepting that the compiler is no longer checking; it doesn't mean corrupting the internal consistency of the type system by introducing human error into the type system.

So okay to turn off the type system to get the flexibility you need to interopt with JavaScript in the wild, but not okay to inject into the type system unenforced types. For example, afaik in Java even casts are checked against runtime-type-information, to insure the cast doesn't violate the invariants in the type system, i.e. you can get a runtime exception with a cast. In C, you can cast an integer to a pointer and a pointer to integer, but C has no range checking on pointers any way, so no internal consistency of the type system is violated by this human error. The runtime of C for pointers is always unsound because pointer bounds are out-of-scope of the type system, i.e. the type system is turned off for pointer bounds. And unsurprisingly the largest class of bugs in C programs are pointer bugs.

aluanhaddad commented 7 years ago

Most languages have type systems are not 100% sound. They have various constructs which bypass or overrule the type checker. Some examples of this include covariant mutable collections, arbitrary casts (sometimes by way of intermediate casts), generic erasure, and Scalas's uncheckedVariance annotation.

TypeScript's type system is not and does not claim to be 100% sound, and it has many of the unsound constructs above, arguably for very practical reasons.

However, structural typing is not an unsoundness it is a deliberate choice which specifies the semantics of what it means to say that a value x is of a type T.

TypeScript was designed in part to formally specify JavaScript's type system. JavaScript does not have manifest type system. typeof is a type level operator but it returns a string because it cannot return a type. People generally regard instanceof as a type level operator but in fact both of its operands are always values. JavaScript is effectively a duck typed language. That it is interested in the shape and not the heritage of objects.

Structural typing allows TypeScript to model this very effectively.

shelby3 commented 7 years ago

How many times (this is the 3rd or 4th time already) am I going to have to repeat the distinction between bypassing (i.e. turning off) the typechecker, versus introducing unchecked semantic meaning into types violating consistency of types. The salient distinction is lifting user error to the enforcement of the semantic consistency of types, not just unchecked runtime values. Turning off typing is not the same as infiltrating the types with unchecked invariants that aren't congruent with those types. Once you take values out of the type system, those values can never safely come back into the type system, except at runtime the compiler can perform a runtime verification of the invariants and throw a runtime exception on failure.

Relaxing what we expect the type system to check does not make the type system (internally) unsound.


Edit: in this issue's case, we are enabling the programmer to inject into the typing system the interface supertype of a type, removing the compiler's ability to check whether that interface supertype is valid. But this isn't just runtime behavior that is unchecked (compiler bypassed), but also the compiler's knowledge of the type has been rendered inconsistent, so that every where the compiler applies that inconsistency, it will also impact compiler checking.

And do note that even on the untyped (uni-typed) ECMAScript, the prototype chain is form of heritage of objects since it is global to all constructed instances which didn't override the prototype as constructed (and even retroactively so which is the point I want to explore modeling with typeclasses for potentially amazing benefits in productivity and extensibility).

shelby3 commented 7 years ago

@mintern wrote:

I agree, while acknowledging that the underlying language, JavaScript is permissive and loosely typed. Specifically quoting another TypeScript non-goal:

3) Apply a sound or "provably correct" type system. Instead, strike a balance between correctness and productivity.

I interpreted that design goal to mean that the type system wouldn't check everything and that absolute runtime (external) soundness isn't a realistic goal for the full range of the JavaScript universe (libraries, frameworks, etc) as it is today. And I agree.

I did not interpret it to mean that we should purposefully inject unenforced types into the type system to destroy the internal consistency soundness of the type system, potentially turning it into random soup of contagion of the chaotic interaction of multiple heuristics in an eventual exponential explosion of Complexity of Whoops, i.e. loss of productivity.

Again I find the design goals document to be somewhat ambiguous and/or self-conflicted. I don't know what the purpose of building a type system with internal inconsistency would be. I guess some sort of hit-and-miss hint system, hoping it provides more hits than misses over time. Do we have the long-term track record of any prior examples to compare to? Again afaics comparing to turning off (i.e. escaping out of, bypassing) the type system is not analogous.

shelby3 commented 7 years ago

@shelby3 wrote:

The salient distinction is lifting user error to the enforcement of the semantic consistency of types, not just unchecked runtime values.

Thinking about if the feature enabled by this issue is not first-class, and thus unable to infiltrate the rest of the type system, thus cordoning the impact of any human error. It appears to not be first-class, except perhaps for example maybe the narrowed type can be used along with partial function application (does TypeScript have partial function application?) to select which of an overloaded function (or method) is saved to a callback instance, which is then passed around as a first-class function which can leak into any part of the type system.

Also thinking about @aluanhaddad's point that when interopting with JavaScript that didn't populate the prototype chain with interfaces per my alternative suggestion for a compiler-assisted and enforced check, it would be perhaps less globally conflated to set a member property per the feature of this issue versus setting up the global prototype chain of a constructor function. Yet wouldn't the TypeScript code which depends on this instanceof be forced to import the module for the declaration of the type it is employing, thus wouldn't the prototype chain be guaranteed to exist in the global scope? I mean if at runtime, the caller can violate the expected type, then caller can violate List and BaseCollection also, not just the interfaces which TypeScript doesn't currently add to the prototype chain.

So again I arrive at the same conclusion, which is that if we want to use TypeScript typing, then we have no guarantees about runtime soundness any way, unless we can assume TypeScript has been employed globally in your ECMAScript universe. So I see no benefit to destroying the soundness of the internal consistency of the TypeScript type system. If the caller isn't interopting with the TypeScript type system where the callee is depending on it, then there will be runtime errors. Enabling some heuristic convenience user-guard so that callers which don't want to set the prototype chain can instead set some member property on the specific instance, is moving us towards utter chaos if such heuristics proliterate. Where do you draw a line and stop creating special case interactions which render the type system inconsistent?

One can I guess argue that since the JavaScript universe outside of TypeScript does not have prototype interfaces (unless setting the prototype chain with interfaces becomes a common programming paradigm), then the adhoc paradigm employed in practice has been to set a member property on an instance to tag it as having a certain interface and thus TypeScript should emulate this form of user-defined, adhoc, inconsistent (compiler unchecked) "typing", so as to interopt with this paradigm, i.e. sacrificing correctness for an increase in interopt productivity. Yet OTOH, we have to opportunity to influence the JavaScript universe to start using the prototype chain for what it is capable of, and keep the TypeState typing system internally consistent, which in another perspective is a greater advance of productivity by not ending up over time with a typing system that is spaghetti contagion of Complexity of Whoops random noise rendering the type system useless.

Edit: in this issue's case, we are enabling the programmer to inject into the typing system the interface supertype of a type, removing the compiler's ability to check whether that interface supertype is valid. But this isn't just runtime behavior that is unchecked (compiler bypassed), but also the compiler's knowledge of the type has been rendered inconsistent, so that every where the compiler applies that inconsistency, it will also impact compiler checking.

I am all for easing interoption with the untyped JavaScript universe when it doesn't render the internal type system inconsistent. In my analysis it is okay to forsake runtime soundness, because the only way we will get very strong runtime soundness is to have the type system every where, i.e. perhaps Google's SoundScript in the VM in the remote future (i.e. a different language than what ECMAScript is now). But I am not seeing how corrupting the type system will lead to long-term stable productivity increase (the type system can potentially become so brittle and inconsistent over time into a clusterfuck). I am making a distinction between bypassing the type system for unchecked runtime behavior and injecting inconsistency into the type system itself. Is that distinction a mirage? I mean is there no way to bypass the type system when needed, which doesn't also impact the internal consistency of the type system? Am I only appealing to subjective religion?

shelby3 commented 7 years ago

I offered an idea about populating the prototype chain with interfaces as a more JavaScript idiomatic paradigm (seems that is the purpose of prototype, thus the idiomatic place to tag the interface structure) and which doesn't break the internal consistency of the TypeScript type system, as an alternative to this issue's adhoc choice of setting member properties on specific instances without any enforcement by JavaScript's prototyped instance construction mechanism, thus breaking the internal consistency of TypeScript while also being non-idiomatic for non-TypeScript code. For me, that already appears to be a slamdunk "no brainer".

There is potentially another way to do this though. That is for the compiler to make a new type check (not instanceof which relies on the prototype chain) which has the semantics of checking what the compiler knows which may be erased at runtime. And employ this new feature (keyword?) to do guards. The JavaScript runtime would be completely oblivious to this erased information and the non-TypeScript code wouldn't be able perform these checks at runtime, but it also wouldn't need to interopt on the requirements for the prototype chain.

And there is yet another way, which I am surprised that no one offered instead of this issue's feature:

@shelby3 wrote:

Edit: and pertaining to this thread's issue feature, if you are checking type structurally, then narrow the type using a guard via structural matching instead of nominally via instanceof or even with the property tag that this issue's feature requires.

If interfaces are intended to be purely structural, then there is no need to tag with a property and add an _ is Type feature (for the purpose of narrowing of type at the guard). Simply match on structure at the guard instead (and use the result of the match to narrow the type). So you'd need some new compiler provided function that performs this structural match similar to my second paragraph in this comment. And at compile-time, this function need not be called because the compiler can infer the type. At run-time, the function would actually need to be called to do a structural matching to check for the interface type .


Meanwhile, I feel like everyone else has been patiently trying to describe TypeScript's motivations, design decisions, and philosophy.

And I have been patiently discussing, except for the attempts to attack me starting with the first reply to me in this thread by @kitsonk that didn't discuss any technicals and instead was calling me out personally as wasting my time and effort. He then clarified that his intention was concern for me, and I thanked him for clarifying. Then @RyanCavanaugh accused me of basing my concern on "feelings" and I strongly rebuked this, because it was an afront to my desire to apply objectivity. Then @aluanhaddad went ballastic on snide comment stating I wasn't sincere about not employing my feelings. And then he piled on after I replied in kind pointing out to him that perhaps he doesn't yet have the maturity to understand how to contain his ego and tribal hindbrain, and the other packdogs have joined in the defense of the tribe. This social pattern is so common in groups, I can always see it coming. I knew right from the moment that @kitsonk posted, it had likely begun but I tried to hope "maybe not this time". But of course, it is always the same. No community will ever allow independent thought from the outside.

P.S. note the lack of exhaustive discussion of alternatives before this issue's _ is Type feature was chosen and merged, indicates to me a dearth of sufficient diverse participants in this community. Please do not chase away dissenting technical opinions. You need them. Consider the value of learning to be tolerant of others and value their technical disagreement. Greatness requires it.

aluanhaddad commented 7 years ago

Please do not chase away dissenting technical opinions. You need them. Consider the value of learning to be tolerant of others and value their technical disagreement. Greatness requires it.

I couldn't agree more. However, it's perfectly valid to program with functions and object literals and to never use classes or even manually wired inheritance via prototypes.

Simply match on structure at the guard instead (and use the result of the match to narrow the type). So you'd need some new compiler provided function that performs this structural match similar to my second paragraph in this comment. And at compile-time, this function need not be called because the compiler can infer the type. At run-time, the function would actually need to be called to do a structural matching to check for the interface type .

What specifically do you have in mind? Type guards are most often used when type information has been lost because it comes from untyped APIs and unknown sources. If the compiler could always determine the actual type, we wouldn't be having this discussion but that is simply not possible.

And do note that even on the untyped (uni-typed) ECMAScript, the prototype chain is form of heritage of objects since it is global to all constructed instances which didn't override the prototype as constructed (and even retroactively so which is the point I want to explore modeling with typeclasses for potentially amazing benefits in productivity and extensibility).

I would definitely be curious to see your formal type class proposal. I think it is a very interesting point that you bring up here about retroactive modification of the prototype chain. Since the prototype property of an object and the prototype's properties are in fact frequently mutable, an object can change at any point during execution such that it no longer matches it's declared static shape. Furthermore, irrespective of mutability, you can create an object which will pass the check o instanceof Date, but not conform to Date's behavior. This is because JavaScript's instanceof operator has no notion of types it only understands values. Walking the prototype chain and doing reference comparisons is far from reliable.

I think that as you have yourself stated, a different language, one targeting a type aware VM may well be the only way to achieve your goals.

That said, you really should spend some more time with the language and see if it is in fact useful to you before continuing. One important thing to be aware of is that TypeScript's classes are structurally typed not normally typed. This is a structurally typed language.

spion commented 7 years ago

@shelby3

First, you can't populate the prototype chain with interfaces. This will not work because instanceof does not work reliably cross-realm. For example a instanceof Array can return false if the array a came from e.g. another iframe (more generally, another realm). See this discussion

Secondly, npm (the most popular package manager for JavaScript at the moment) compounds the above problem by installing multiple semver-incompatible (and up until npm 2.0, also semver-compatible) versions of the same library into different directories. This in turn means that a class defined in such a module may actually have more than one value; and again instanceof won't work reliably, similarly to the way it doesn't work reliably cross-realm.

Finally, this is simply not how most JavaScript is written. Infact it cannot be how most JS is written, as this would be a typescript-only feature. And here we come to a clash to TypeScript's design goals: to be a type system which helps with existing JavaScript code. Just look at how Promises/A+ thenables are specified. Its all about a method then present on the thenable object. Not about some non-existing constant "Thenable" that should be in the prototype chain. Admittedly, some of this is a product of the other two instanceof problems above. The rest of the reasons are complex, but mainly its a combination of "no single module system" (this constant would need to be defined in some JS module and exported from it), desire to keep JS code small and therefore devoid of dependencies, desire for interoperability etc. Nevertheless, these reasons confine most JS code to structural checking (and since TypeScript aims to model JS code, its therefore confined to structural types)

As to why type guards are okay, I'll just quote myself without the "you are wrong" part:

This sort of feature is precisely what makes TypeScript different from other typed languages. While they decide NOT to trust the user unless their input fits their narrow preconceptions of what is correctly modelled code, TypeScript takes a different approach. TypeScript mostly trusts the user. The user, in turn, needs to take special care in ensuring some of their code is correct, like type guards. But not all, as it would be with dynamic languages.

This is the biggest win of TypeScript: it lets you pick between "great power, great responsibility" (carefully hand-check the code, ensure that your type assertions are correct) and "lesser power, lesser responsibility" (once you hand-"prove" type assertions are correct, you don't have to do it for the rest of the code).

Its not a type system that ensures correctness. Its a type system that works with the user to ensure it. Its a "bring your own lemma" type system

So this feature is sound in this sense: "The compiler cannot automatically check this, but if you supply your own unchecked proof that the type is indeed correct, it will accept that". This is still useful, as the code that needs to be carefully checked by a human is confined to a type guard.

shelby3 commented 7 years ago

@aluanhaddad wrote:

However, it's perfectly valid to program with functions and object literals and to never use classes or even manually wired inheritance via prototypes.

Agreed. That is why I mentioned the purely structural option as an alternative to the feature of this issue which was adopted.

It is difficult to have an open discussion and ignore discussion. Therefor...

First, I am unblocking you (perhaps contrary to my better judgement) because it seems I can trust you to talk technicals (and you may have valuable discussion to share) and to not to involve me in discrimination claims. If that changes, I may regrettably be forced to backtrack. I am not stating this as if I desire any authority or control over you (nor to insinuate any judgement of blame or correctness), rather this is just a statement of my personal policy w.r.t. to you. Please avoid making any insinuations that would cause me to consider a legal protection stance to be more important than open discussion. For me, open discussion is paramount, but I do have to be pragmatic in this era where we all commit 3 felonies per day just by breathing.

Simply match on structure at the guard instead (and use the result of the match to narrow the type). So you'd need some new compiler provided function that performs this structural match similar to my second paragraph in this comment. And at compile-time, this function need not be called because the compiler can infer the type. At run-time, the function would actually need to be called to do a structural matching to check for the interface type .

What specifically do you have in mind? Type guards are most often used when type information has been lost because it comes from untyped APIs and unknown sources. If the compiler could always determine the actual type, we wouldn't be having this discussion but that is simply not possible.

if (someNode.isA(Sortable) {
    someNode.sort()
}

Note the compiler has type checked that at compile-time in the above case.

The compiler would emit:

if (someNode.isA({ sort:function() {} }) {
    someNode.sort()
}

So the isA function would check that the properties of the interface match structurally up the capabilities of what the runtime can check structurally (no instanceof nominal checks in this strategy), e.g. the existence of the sort property, that it has a typeof x == 'function', and the number of parameters of the function.

That seems to be much more sane than the feature that was adopted, because at least it enforces structural type checking at compile-time (rather than depending on human error) and even marginal structural type checking at runtime.

Note if there is no else case on the guard, I presume by default it should throw an exception at runtime if the if condition is false.

Perhaps a compiler option would be to omit the runtime checks, then the programmer is confident their runtime environment is cordoned soundly.

@spion wrote:

So this feature is sound in this sense: "The compiler cannot automatically check this, but if you supply your own unchecked proof that the type is indeed correct, it will accept that". This is still useful, as the code that needs to be carefully checked by a human is confined to a type guard.

Maybe useful to some but terribly unsound because it breaks the internal consistency of the TypeScript type system (not just bypassing it to enable runtime unsoundness), and I believe I have shown above that there is another way that wouldn't break the internal consistency of the TypeScript type system.

shelby3 commented 7 years ago

The following concerns the nominal typing idea I promulgated in this thread, which is orthogonal to the prior comment of mine explaining a purely structural idea.

@spion wrote:

First, you can't populate the prototype chain with interfaces. This will not work because instanceof does not work reliably cross-realm.

Structural typing can fail also due to false positive matches (both at compile-time and runtime). Nominal typing can fail dynamically at runtime (due to changes to the prototype chain or as you explained below), but not at compile-time.

Choose your poison.

For example a instanceof Array can return false if the array a came from e.g. another iframe (more generally, another realm). See this discussion

Yeah I was aware of that from this, and thanks for citing that source which explains it more completely.

Secondly, npm (the most popular package manager for JavaScript at the moment) compounds the above problem by installing multiple semver-incompatible (and up until npm 2.0, also semver-compatible) versions of the same library into different directories. This in turn means that a class defined in such a module may actually have more than one value; and again instanceof won't work reliably, similarly to the way it doesn't work reliably cross-realm.

I'd need a more thorough explanation to understand how npm managed to break instanceof, but what an individual framework does to make itself incompatible with one of JavaScript's capabilities, should not preclude us from supporting and not ignoring that capability.

As in all things with JavaScript, the programmer has to be aware and be careful, because JavaScript is a dynamic, highly open ecosystem. Programmers will pressure frameworks in a free market and the free market will work it out. It is not our authority to decide for the free market.

I do not consider these potential pitfalls with nominal typing to be a rational justification to completely avoid nominal typing with JavaScript. As I wrote above, structural typing also has pitfalls. Programmers should have both nominal and structural typing in their toolchest.

It is bogus for anyone to claim that JavaScript is only set up for structural typing. JavaScript has prototype inheritance (Douglas Crockford), which can support nominal typing. The fact that it's globally retroactive and mutable, is one of its features.

Finally, this is simply not how most JavaScript is written.

By 'this', I assume you are referring to employing JavaScript's prototype chain for nominal typing in general, and specifically for instanceof guards.

I doubt very much that instanceof or constructor.name are never used for nominal runtime typing guards in the entire JavaScript universe.

We don't write general purpose programming languages (i.e. TypeScript) to cater only to 90% of the programmers. A general purpose programming language that is supposed to be compatible with JavaScript ecosystem should offer the entire language of capability.

You don't get to decide for the universe. This is an ecosystem and free market.

Infact it cannot be how most JS is written, as this would be a typescript-only feature.

How is supporting a JavaScript feature only a TypeScript-only feature? Offering ways to type the prototype chain is providing a way to interopt (to some degree more than now) with the use of that prototype chain in non-TypeScript software.

You seem to often make declarations of fact which are factually devoid of complete evidence, e.g. "you are wrong", "you are inconsistent", and "in fact it cannot be". Could you please try to be a bit more open-minded and focus on fully proving your arguments (and allowing the possibility that through discussion you might realize otherwise) before declaring them as fact.

And here we come to a clash to TypeScript's design goals: to be a type system which helps with existing JavaScript code.

What is the proven clash? And I don't think the goal is "existing JavaScript code" but rather "the existing ECMAScript standard".

Just look at how Promises/A+ thenables are specified. Its all about a method then present on the thenable object. Not about some non-existing constant "Thenable" that should be in the prototype chain. Admittedly, some of this is a product of the other two instanceof problems above.

In my code, I detect instanceof Promise because I am using ES6 generators to simulate ES7 async / await. You even wrote a recent blog which sort of explains why I prefer to use generators (great minds think alike eh :)

You somehow think you know what every existing JavaScript code in the universe is doing. How did you achieve such omniscience given that the speed-of-light is finite?

The rest of the reasons are complex, but mainly its a combination of "no single module system" (this constant would need to be defined in some JS module and exported from it), desire to keep JS code small and therefore devoid of dependencies, desire for interoperability etc. Nevertheless, these reasons confine most JS code to structural checking (and since TypeScript aims to model JS code, its therefore confined to structural types)

Prototype inheritance is inherently locally coherent and modular because it is object-based, so there doesn't need to be any global coherence. Could you please explain more clearly what problem you envision?

spion commented 7 years ago

I do not consider these potential pitfalls with nominal typing to be a rational justification to completely avoid nominal typing with JavaScript. As I wrote above, structural typing also has pitfalls. Programmers should have both nominal and structural typing in their toolchest.

You can consider it whatever you want, the fact is that the kind of JS that developers normally write is based on basic structural checking, and as such its TypeScript's primary job to support that.

Additionally, nominal type systems suffer from the "interface afterthought" problem. Example:

  1. First promise library P1 appears, written by author A1
  2. A1 never gave second thought to the "Thenable" idea, so they never wrote a separate "Thenable" (interface) module.
  3. Another author A2 writes a better promise library P2. Due to the nature of nominal types, its incompatible with A1
  4. Because of incompatibility with P1, P2 cannot achieve enough traction and adaptation without explicitly depending on P1 or inheriting from P1, as a lot of things use P1, and P1 uses nominal checking to ensure correct types.
  5. Frustrated by this, A2 writes Thenable module, and urges A1 to adopt it.
  6. A1 sees no value in this, due to the fact that library P2 doesn't have enough traction to justify a change in P1, and due to the fact that from their point of view P1 is obviously better.
  7. The development of promises has stalled.

Regarding your structural check idea, can you please tell me what the cost will be to check the following?

interface Node {
  __special_tag_to_check_if_value_is_node: string;
  data: <T>
  children: Array<Node<T>>
}

Because with type guards, I can make it be O(1) and be reasonably sure its correct unless someone is trying to deliberately subvert it.

shelby3 commented 7 years ago

@spion wrote:

You can consider it whatever you want, the fact is that the kind of JS that developers normally write is based on basic structural checking, and as such its TypeScript's primary job to support that.

Please re-read my prior comment as I have rebutted this "normally" argument.

Additionally, nominal type systems suffer from the "interface afterthought" problem. Example:

You are conflating nominal typing with subclassing. That is why I am preparing to promulgate typeclasses typing of the prototype chain. I am hopefully going to radically impact the JavaScript universe on a significant scale. TypeScript can come along for the ride or it can be obstinate. Either way, I am going to see this concept gets implemented (eventually), unless I discover it is flawed. I've been working on this concept for past several years (on and off) and very intensely this past May. If I can get others interested now, that would be best.

spion commented 7 years ago

I do not consider these potential pitfalls with nominal typing to be a rational justification to completely avoid nominal typing with JavaScript. As I wrote above, structural typing also has pitfalls. Programmers should have both nominal and structural typing in their toolchest.

The difference in pitfalls is fundamental. With instanceof checks, code that is supposed to work breaks. With structural checks, code that is not supposed to work breaks at run time, rather than compile time.

shelby3 commented 7 years ago

@spion wrote:

The difference in pitfalls is fundamental. With instanceof checks, code that is supposed to work breaks. With structural checks, code that is not supposed to work breaks at run time, rather than compile time.

That is an interesting perspective, but it depends on who and what was "supposed to". If instanceof breaks, is it because the programmer was supposed to be aware of the couple of general ways it can fail and avoid them? So then was it supposed to work or not supposed to work?

Your logic presumes that structural code is suppose to fail because the programmer designed the code wrong, but wasn't he supposed to design it correctly? And structural code can fail at compile-time, if we presume that not having the ability to distinguish between nominal intent and structure as a failure of structural compile-time typing as compared to nominal.

I hope you are somewhat convinced that your choices were somewhat arbitrary.

Apparently one of the ways I piss people off without even trying to, is I think much more generally (or let's say I just keep thinking and don't assume I've ever finalized my understanding) and they just can't understand why I don't adhere to the very obvious viewpoint that they think is the only possible one. Unfortunately I am not smart enough to be able to both think generally and find a way to hide it and bring it out in a politically astute way making others think that I adhered to their view and then we together generalized it together (or some political methodology like that). I tend to be too matter-of-fact, especially when I am at the bandwidth limit of my capabilities.

spion commented 7 years ago

Prototype inheritance is inherently locally coherent and modular because it is object-based, so there doesn't need to be any global coherence. Could you please explain more clearly what problem you envision?

Promises/A+ thenables are a good example. If you want to specify a Thenable nominal interface, there needs to be a single value that represents it in the prototype chain (in order for instanceof to work). To get this single value into all libraries that implement Thenable, it needs to be a module of a module system that guarantees a single instance will be delivered when requested via import. AFAIC This is not guaranteed by either ES6 modules or CommonJS modules, so at best you would need to ensure it in the module loader spec, and any environment that uses different loaders as well (nodejs?)

Btw, ES6 tried and failed to solve this problem (for users) with Symbols. The final solution ended up being a string-addressed global symbol registry.

spion commented 7 years ago

@shelby3

That is an interesting perspective, but it depends on who and what was "supposed to". If instanceof breaks, is it because the programmer was supposed to be aware of the couple of general ways it can fail and avoid them? So then was it supposed to work or not supposed to work?

That would mean avoiding instanceof to check arguments passed externally, as they may come from another realm. Which means avoiding its use as a type guard in many cases where such arguments may come externally (e.g. a library accepting arguments provided by the consumer would not be able to use this)

edit: removed problematic section.

shelby3 commented 7 years ago

@spion wrote:

That is an interesting perspective, but it depends on who and what was "supposed to". If instanceof breaks, is it because the programmer was supposed to be aware of the couple of general ways it can fail and avoid them? So then was it supposed to work or not supposed to work?

That would mean avoiding instanceof to check arguments passed externally, as they may come from another realm. Which means avoiding its use as a type guard.

Or avoiding the other realms. We shouldn't presume the only use of ECMAScript is in broken realms such as the browser and NPM. The (expanse of possibilities in the unpredictable future of the) universe is not so tiny.

I believe you are somewhat oversimplifying and pigeonholing your case studies (which is a problem if we extrapolate these as the only truth worth caring about).

The language can be orthogonal to realms. For example using ECMAScript to code mobile apps. (which is one of the reasons I am here)

I won't disagree that the browser and NPM are huge and important realms today, but even today they are not 100% of the JS ecosystem.

We also can't know if those existing huge realms won't fix themselves when under free market pressure to do so, or be superceded by larger new realms.

We shouldn't conclude the language features are eternally broken just because some huge legacy realms (which I believe are dying) broke those features.

RyanCavanaugh commented 7 years ago

Previous comments here have been running afoul of the Code of Conduct, but I appreciate everyone redirecting their attention to the technical discussion at hand. Let's keep it that way.

shelby3 commented 7 years ago

@spion wrote:

Prototype inheritance is inherently locally coherent and modular because it is object-based, so there doesn't need to be any global coherence. Could you please explain more clearly what problem you envision?

Promises/A+ thenables are a good example. If you want to specify a Thenable nominal interface, there needs to be a single value that represents it in the prototype chain (in order for instanceof to work).

If we are referring to subclassing and not typeclasses, the Promise constructor function controls what will be put in the prototype chain. Even if you import multiple instances of a Promise, they will all for each use only one Thenable interface per prototype chain. But with redundant imports, all of these Thenable interfaces will not have the same reference in memory, since we'd have multiple instances of the Promise constructor function. So I agree that non-redundant imports are necessary if we expect to have a unified instanceoffor all instances if we are basing instanceof on matching instance by reference in memory and not matching names (and possibly the source code) of the constructor function.

To get this single value into all libraries that implement Thenable, it needs to be a module of a module system that guarantees a single instance will be delivered when requested via import.

Yes and I designed such an import system for my coding, but it doesn't solve the cross-realm issue. And this would require me to be sure all libraries I use which can pass my code a Promise also use a consistent importing system that enforces non-redundant imports.

A consistent importing system wide is important. I agree but not if the other possibilities mentioned above and below can work sufficiently well.

AFAIC This is not guaranteed by either ES6 modules or CommonJS modules, so at best you would need to ensure it in the module loader spec, and any environment that uses different loaders as well (nodejs?)

I understand there are broken legacy realms. C'est la vie. We move forward anyway.

Btw, ES6 tried and failed to solve this problem (for users) with Symbols. The final solution ended up being a string-addressed global symbol registry.

Instead of string keys, they could have used 160-bit cryptographic hashes (or any approximation to a random oracle) to be probabilistically sure of no collisions.

Perhaps they needed me around to suggest that? I find it difficult to imagine that no one else would have thought of using hashes to solve the problem of global collisions.

And this seems it would be a good way to make name space issues orthogonal to the module import system.

Did it fail because of name space collisions, lack of adoption, or what?

spion commented 7 years ago

Or avoiding the other realms. We shouldn't presume the only use of ECMAScript is in broken realms such as the browser and NPM. The (expanse of possibilities in the unpredictable future of the) universe is not so tiny.

This is not what realms means. A realm can be thought of as a fresh global "scope". For example an iframe has a different "realm" from its parent window. That means it has different unique global (window) object, as well as other globals: e.g. Array constructor and array prototype. As a result, passing an array you got from an iframe to a function that checks instanceof Array means the array will fail the check.

This is not merely a theoretical concern

In CommonJS, every module is wrapped by a header and footer that form a closure:

function(module, exports, ...) {

<module code here>

}

Which is then called with an empty exports and initialized module object, at discretion, by the module system.

Its the same problem with e.g. class expressions:

function makeClass() {
  return class C {
    // definition here
  }
}

let C1 = makeClass(), C2 = makeClass(), c1 = new C1(), c2 = new C2();

assert(c1 instanceof C2) // fails
assert(c2 instanceof C1) // fails

Did it fail because of name space collisions, lack of adoption, or what?

It failed because they came up with a neat little way to define unique constants that don't clash with anything existing and can be used as object keys, then wanted to expose this mechanism to users somehow, but failed to take cross-realm issues into account. The keys were now too unique: user code that executed in each realm generated its own; only language-defined ones were guaranteed to be the same cross-realm. So we're back to string keys, which is where we were in the first place before Symbols entered the scene.

By all means, take 160-bit cryptographic hashes idea to esdiscuss. May I ask though, exactly what is the thing that you plan to hash to get the unique key that solves the multi-realm problem?

yortus commented 7 years ago

As of ES6, instanceof is decoupled from the prototype chain due to Symbol.hasInstance. Walking the prototype chain is now just the default behaviour. But in obj instanceof Obj, if the Obj value has its own [Symbol.hasInstance] property, then that will determine how instanceof behaves.

eggers commented 7 years ago

@shelby3 TypeScript wasn't meant to be a new language. It was meant to just add types on top of Javascript. One of the current trends of JavaScript is something called DuckTyping. (If it walks like a Duck and quacks like a Duck, then it's a Duck.) That is a very different concept than inheritance and is in fact antithetical to it. Interfaces are TypeScripts answer to DuckTyping. Putting Interfaces on the prototype chain would defeat their purpose. User defined type guards were meant to solve type guards for interfaces. Maybe there is a better way of creating them. (I personally would prefer that they were more tightly bound to the interfaces themselves.) However, user defined type guards definitely belong in TypeScript, and they definitely don't belong on the prototype chain.

/* DuckTyping */

interface Foo { foo:string };

function bar(foo: Foo) {
  // something
}

var foo = {foo: 'bar'};
bar(foo); // Legal even though Foo was never explicitly implemented.
/* Multiple Inheritance */

interface Car {
  goto(dest: Land): void;
}

interface Boat {
  goto(des: Water): void;
}

class HoverCraft {
  goto(dest: Water|Land) {
    // something
  }
}

@yortus That's awesome. Maybe a separate mechanism for user defined type guards could be implemented that could be down compiled as the current type guards are. I personally think that it would be more intuitive to do write something like this (The type guards should be more closely bound to the interfaces than they currently are):

interface Cat {
  name: string;
  static [Symbol.hasInstance](animal: Animal) {
    return a.name === 'kitty';
  }
}

if (c instanceof Cat) { // or maybe `c implements Cat`
  // dog barks.
}

which could could compile to:

// es6
class Cat {  
  static [Symbol.hasInstance](instance) {
    return a.name === 'kitty';
  }
}

if (c instanceof Cat) {
  // dog barks.
}

// es5

var Cat = (function () {
    function Cat() {
    }
    Cat[Symbol.hasInstance] = function (instance) {
        return a.name === 'kitty';
    };
    return Cat;
}());

if (Cat[Symbol.hasInstance(c)) {
  // dog barks.
}
shelby3 commented 7 years ago

@spion wrote:

Or avoiding the other realms. We shouldn't presume the only use of ECMAScript is in broken realms such as the browser and NPM. The (expanse of possibilities in the unpredictable future of the) universe is not so tiny.

This is not what realms means.

I did not define 'realms'.

A realm can be thought of as a fresh global "scope". For example an iframe has a different "realm" from its parent window.

I claim it is evident that I knew that by noticing that "avoiding" that problem could involve "avoiding ... broken realms such as [in] the browser". The point is that if the browser is creating these fresh global "scopes" without some mechanism such as Symbol (which btw I wasn't aware of until you mentioned it) to fix the problem, then the browser is a promulgator of broken design w.r.t. to realms.

I do not presume that the problem with realms can't be fixed any where. I am not claiming you presume it can't. If you are confident it is broken every where and/or can't or won't be fixed every where (or no where of significance from your perspective), I am very much interested to read your explanation. I am presuming until you specify otherwise, that your predominant concern is the global "scopes" (realms) issue. I realize you are also concerned about existing popular module systems.

In CommonJS, every module is ...

It is possible to insure every module is only instantiated once within the same realm "scope". I have module code doing it. It may or may not be possible with CommonJS and other existing modules. I haven't looked into that yet.

Its the same problem with e.g. class expressions:

What is the problem you envision? If the module for the function makeClass() was only instantiated once, then by default all instances will have the same prototype and [Symbol.hasInstance] properties.

By all means, take 160-bit cryptographic hashes idea to esdiscuss. May I ask though, exactly what is the thing that you plan to hash to get the unique key that solves the multi-realm problem?

Yeah I realized today while I was driving, that in my sleepless state I had forgotten to specify what gets hashed. It would need to be the entire module's code concatenated with a nonce incremented for each unique key requested by that module.

shelby3 commented 7 years ago

@eggers please note I have made three different possible suggestions to choose from. One of them is to use purely structural matching for the user guard (which afaics appears to fix the serious soundness flaws that this issue's "fix" created), so I am not advocating putting any interface in the prototype chain for that suggestion.

@yortus thank you.

shelby3 commented 7 years ago

@aluanhaddad wrote:

I would definitely be curious to see your formal type class proposal. ... Walking the prototype chain and doing reference comparisons is far from reliable.

I think that as you have yourself stated, a different language, one targeting a type aware VM may well be the only way to achieve your goals.

To the extent that TypeScript can embrace unreliability of expected structural (interface and subclassed) types at runtime (even possibly with optional runtime nominal and structural checks that exception to a default or error case), I think perhaps the similar level of typing assurances can be attained with typeclasses. Typeclasses would only make sense nominally, as otherwise they are same as the structural interfaces we have already.

And I believe typeclasses are much more flexible for extension, compared to subclassing. I intend to attempt to explain that soon in the issue thread I created for that.

If we are going to add some nominal capabilities that are compatible with JavaScript's existing paradigms, such as instanceof, then typeclasses would give us the flexibility of extension we get with structural types. Note that instanceof may become much more reliable.

P.S. you are referring to the comment I made about Google's SoundScript and that if they succeed to accomplish runtime soundness, I believe it will essentially be a much different language.

eggers commented 7 years ago

@shelby3 Ah, I missed that structural proposal. There has been a lot to read the last couple of days in here.

I actually do think something like that would work. It would add some runtime overhead for a large interface as checking for the existence of many fields would take some time, but probably not prohibitive. By default the TypeScript compiler could out put code that checked for all properties/functions, with the option of overriding [Symbol.hasInstance] with a custom check. However rather than using a special function on interfaces isA(x), I would use a keyword like implements or implementationOf, maybe overloading instanceOf.

shelby3 commented 7 years ago

@eggers wrote:

By default the TypeScript compiler could out put code that checked for all properties/functions, with the option of overriding [Symbol.hasInstance] with a custom check.

Also, when the compiler constructed the instance within the function, then it could optimize away the runtime structural check.

eggers commented 7 years ago

@shelby3 can you explain more about how it could optimize away the structural check? If you need to do one thing if it's an implementation of Animal and another if it's one of Vehicle, you still need to structurally check which object it implements.

shelby3 commented 7 years ago

@eggers when the compiler knows that the instance was constructed within the function, then it knows at runtime it has to be of the type that was constructed, thus it doesn't need to do any runtime check for the structural type.

Also I want to add that afaics ideas for tagging the structural type (i.e. roughly a simulation for nominal type) instead of checking its structure (which I presume exist to increase performance), such as the feature that was implemented for this issue #1007, are afaics breaking structural type checking. And the feature of #1007 is even worse IMO, because it additionally breaks the internal consistency of the compiler because it relied on the human error to tell the compiler what the type of a (metadata) tag corresponds to.

Today (since I have now caught up on some sleep) I am going to be initiating+participating in a more holistic analysis of all this and tying it into the nominal typing discussion, as well as my proposal for typeclasses. I'll try to remember to cross-link from this issue discussion. I also will learn more about the adhoc tagging paradigm that has been adopted by JS frameworks and libraries. This is a learning process for me as well.

nickredmark commented 6 years ago

Is there a way to define a type guard that activates a type if it returns? Something that would allow you to write something like this:

try {
   checkIsA(o)
   // from here on o has type A
} catch(e) {
}
kitsonk commented 6 years ago

This is not a support forum.

Questions should be asked at StackOverflow or on Gitter.im.

yortus commented 6 years ago

@nmaro it has been suggested but not implemented so far. See for example #8655, and other issues linked from there.