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.
The second example does not seem to scale very well :
class Node {
isLeafNode(): this is LeafNode { throw new Error('abstract'); }
isOtherNode(): this is OtherNode { throw new Error('abstract'); }
}
class ParentNode extends Node {
isLeafNode(): this is LeafNode { return false; }
isOtherNode(): this is OtherNode { return false; }
}
class OtherNode extends Node {
isLeafNode(): this is LeafNode { return false; }
isOtherNode(): this is OtherNode { return true; }
}
class LeafNode extends Node {
isLeafNode(): this is LeafNode { return true; }
isOtherNode(): this is OtherNode { return false; }
}
var someNode: LeafNode|ParentNode|OtherNode;
if(someNode.isLeafNode()) {
// someNode: LeafNode in this block
}
Furthermore, we are back with typing that is dependent on class construction while one would expect this to work with interface only for items as simple as these.
Finally, given that we are stuck with classes, we can currently encode this idiom more lightly with instanceof :
class SuperNode { }
class ParentNode extends SuperNode {
private constrain; // using dummy private to disjoint classes
}
class OtherNode extends SuperNode {
private constrain;
}
class LeafNode extends SuperNode {
private constrain;
leafNode : string;
}
var someNode : ParentNode | OtherNode | LeafNode;
if(someNode instanceof LeafNode) {
someNode.leafNode;
}
The first example seems to be an excellent case for #1003
Perhaps the second example was not clear in its intent. Consider something like this, where instanceof
would not work:
interface Sortable {
sort(): void;
}
class BaseCollection {
isSortable(): this is Sortable { return false; }
}
class List extends BaseCollection implements Sortable {
isSortable(): this is Sortable { return true; }
sort() { ... }
}
class HashSet extends BaseCollection {
isSortable(): this is Sortable { return false; }
}
class LinkedList extends BaseCollection implements Sortable {
isSortable(): this is Sortable { return true; }
sort() { ... }
}
Indeed its intent is indeed clearer. So I have two comments :
1) With the current compiler, we can leverage that everything is either "undefined" or something to create an optional conversion :
interface Sortable {
sort(): void;
}
class BaseCollection {
asSortable() : Sortable { return undefined }
}
class List extends BaseCollection implements Sortable {
asSortable() { return this }
sort(){}
}
class HashSet extends BaseCollection {
}
class LinkedList extends BaseCollection implements Sortable {
asSortable() { return this; }
sort() { }
}
function someFun(collection : BaseCollection) {
var asSortable = collection.asSortable();
if(asSortable) {
asSortable.sort();
}
}
But I agree that it would be strange to do something through asSortable and then come back on collection to call other methods.
2) I assume that the compiler verifies that the result of "this is Sortable" is in accordance with what the class actually implements (the verification might be done structurally). In that case, what about generating the "return true" ?
class List extends BaseCollection implements Sortable {
isSortable() : this is Sortable; // would return true
sort() { ... }
}
class OtherList extends BaseCollection {
isSortable() : this is Sortable; // would return true
sort() { ... }
}
class OtherList2 extends BaseCollection {
isSortable() : this is Sortable; // would return false
}
class OtherList3 {
isSortable() : this is Sortable; // would return true
sort() { ... }
}
class OtherList4 {
isSortable() : this is Sortable; // would return false
}
This is a cool idea. Have you considered combining it with generics? Then you could do something like this for array checks:
function isNumber(nbr): nbr is number {
return typeof nbr === 'number';
}
function isString(str): str is string {
return typeof str === 'string';
}
function isArrayOf<T>(of: (item) => item is T, a: any[]): a is T[] {
// Ensure that there's at least one item of T in the array, and all others are of T too.
}
function isArrayOrEmptyOf<T>(of: (item) => item is T, a: any[]): a is T[] {
// Accepts an empty array, or where all items are of T.
}
function(input: string|string[]|number[]) {
if (isString(input)) {
// It's a string.
}
else if (isArrayOrEmptyOf(isString, input)) {
// It's an array of string, or
// an empty array (could be either of string or number!)
}
else if (isArrayOf(isNumber, input)) {
// It's an array of number.
}
}
@bgever We did indeed consider that. It breaks down to (a) allowing x is T
where T
is a type parameter, and (b) extending type inference such that a type argument can be inferred from the target type of a user defined type guard function. All up I think this could be really useful.
One example usage for the standard library - Array.isArray can be changed to:
interface ArrayConstructor {
isArray<T>(arg: any): arg is T[];
}
@Arnavion I believe that for Array.isArray it would be more correct to express in a non-generic way:
interface ArrayConstructor {
isArray(arg: any): arg is any[];
}
This would support union types like MyCustomObject | any[]
And how about:
interface Object {
hasOwnProperty<T | any>(v: string): v is T;
}
Object.hasOwnProperty<MyCustomObject>.call(obj, 'id');
Or it could narrow to possible matching types obj
could be:
var obj: { id: number; description: string; } | any;
if ('id' in obj) {
// obj.id is valid here
}
@awerlang
Actually, I suppose isArray<T>(): T[]
would give the false assurance that the array only has elements of type T. It might be better to require the user to give an explicit assertion <T[]>
so that they know they have to perform some validation after the call to isArray to be sure they actually have a T[].
Your last example results in obj being of type any because of union type behavior, so obj.id is also allowed already. If you meant you wanted obj to be considered of the first type in the union (and thus obj.id would have type number) inside the if block, then that doesn't seem right. obj could be { id: "foo" }
which would not be assignable to the first type.
For your second example, I assume you meant hasOwnProperty<T>(obj: T | any, v: string): obj is T;
. This has the same problem - the presence of an 'id' member doesn't really tell you that obj is of MyCustomObject type and not some other type that also has an id property. So it should be something like hasOwnProperty(obj: any, v: string): obj is { [v]: any };
.
Even if the obj was of type { id: number, description: string } | { foo: string }
(i.e., the union doesn't contain any), the second and third example should still result in obj being of type { id: any }
inside the if-block, because obj could actually be { foo: "5", id: "bar" }
which isn't assignable to the first type but is assignable to the second.
@Arnavion
I think having isArray(arg): arg is any[]
is convenient, even though we don't assert element type.
About inferring an object type by duck-typing, I consider it is not a problem in its own, although I see the problems it may lead to. This is a concern in #1427 . How would you handle this?
Sorry, perhaps I wasn't clear. I was agreeing with you that isArray should return arg is any[]
.
And yes, #1427 is exactly the same as what I said for your other two examples.
Here's another use case based on a filesystem Entry as defined in the not-yet-standard File System API:
interface Entry {
isFile: boolean;
isDirectory: boolean;
name: string;
...
}
interface DirectoryEntry extends Entry { ... }
interface FileEntry extends Entry { ... }
When (<Entry>entry).isFile
, entry
is a FileEntry
, and when entry.isDirectory
, entry
is a DirectoryEntry
.
Note that the *Entry
classes are not easily accessible, so instanceof
can't be used.
Approved; assignment to Anders is tentative (someone else can give this a shot if they're feeling up to the challenge)
@RyanCavanaugh I decided to take shot on this. Just one question. Can the type guard function take multiple arguments?
function isCat(a: Animal, b: number): a is Cat {
return a.name === 'kitty';
}
and if so does the argument index need to match the parameter index?
if(isCat(b, a)) {
a.meow();// error
}
No error if matched index:
if(isCat(a, b)) {
a.meow();
}
I'm not sure of its's usefulness though.
Multiple arguments should be allowed.
I don't understand the example, though. What are the types of the unbound variables a
and b
in the bottom two code blocks?
a
is of type Animal
and b just a number. let's assume that isCat takes any
as argument too.
Thanks @tinganho!
:clap: Thank you!
:smiley:
:clap: Great job @tinganho!
@jbondc The scenario you are describing is covered by #1003, not this feature.
I see what you are saying, but it requires a way for the user to express the intended mapping from return values to narrowing types. I think this is not a useful capability in general. Certain special cases are useful though. Type predicates and property accesses with literals are indeed useful cases. They can use different mechanisms because they are different enough.
I'm aware the feature has already been finalized in 1.6, but just wanted to mention a possible alternative syntax using decorators (extended to functions [at least for this case] and evaluated at compile-time):
@inquiresType(Cat)
function isCat(a: Animal): boolean {
return a.name === 'kitty';
}
var x: Animal;
if(isCat(x)) {
x.meow(); // OK, x is Cat in this block
}
class Node {
@inquiresClassType(LeafNode)
isLeafNode(): boolean { throw new Error('abstract'); }
}
class ParentNode extends Node {
@inquiresClassType(LeafNode)
isLeafNode(): boolean { return false; }
}
class LeafNode extends Node {
@inquiresClassType(LeafNode)
isLeafNode(): boolean { return true; }
}
var someNode: LeafNode|ParentNode;
if(someNode.isLeafNode()) {
// someNode: LeafNode in this block
}
I'm not saying the current syntax is bad at all. However, I would imagine that if the feature was proposed today, an approach like this might be seen as more conservative (would have a lower general impact on syntax, and probably be easier to implement in the compiler).
@RyanCavanaugh , the first example says you can use this is LeafNode
, but in reality only the a is Cat
syntax works.
Maybe the example wasn't updated or it's a bug. The merged PR description however leads me to believe this was never implemented.
@use-strict you're correct that it wasn't implemented. Probably best to open a new issue if this is something you'd like to have (so we can gauge how many people are running into it).
@RyanCavanaugh, did support for this is X
make it into the 1.6 release?
class ParentNode extends Node {
isLeafNode(): this is LeafNode { return false; }
}
I'm doing something along the lines of the following, but it appears to be unsupported?
abstract class Animal
{
get isCat(): this is Dog;
}
class Dog extends Animal
{
get isDog(): this is Dog { return true; }
}
EDIT: Whoops, I missed the post above which covered this. I have opened a new issue here: #5764
Show this have solved the error Index signature of object type implicitly has an 'any' type.
for the example below?
var foo = {bar: 'bar'}
var prop = 'bar';
if (foo.hasOwnProperty(prop)) {
console.log(foo[prop])
}
@eggers You'd have to rewrite it slightly, but it works:
function hasProp<T>(x: T, name: string): x is T & { [key: string]: any } {
return x && x.hasOwnProperty(name);
}
var foo = {bar: 'bar'}
var prop = 'bar';
if (hasProp(foo, prop)) {
console.log(foo[prop])
}
@RyanCavanaugh Not quite, because then the below doesn't throw an error like it should:
function hasProp<T>(x: T, name: string): x is T & { [key: string]: any } {
return x && x.hasOwnProperty(name);
}
var foo = {bar: 'bar'}
var prop = 'bar';
if (hasProp(foo, prop)) {
console.log(foo['abc'])
}
I don't understand why the proposed syntax is necessary, nor how it is even sound typing. The compiler isn't able to check the typing assumption.
Unless someone can justify it's existence in the version 1.6 of the compiler, I will never use it and wish there is a compiler flag to prevent its use. And I would prefer it be deprecated asap.
@RyanCavanaugh wrote:
In many cases, users will have their own functions that can provide _run-time type information_.
I suppose there are cases where the types can't be discriminated based on structural typing because the types don't differ in their member properties, but afaics instanceof
would still always work with the prototype
chain of class
. In other words, instanceof
is always nominal, not structural.
What other run-time type information scenarios would justify this syntax?
@RyanCavanaugh wrote:
Perhaps the second example was not clear in its intent. Consider something like this, where
instanceof
would not work:
The following works and there is no unchecked typing assumption:
abstract class Sortable {
abstract sort(): void
}
class BaseCollection {
}
class List extends BaseCollection implements Sortable {
sort() {}
}
var someNode : BaseCollection | List
if(someNode instanceof Sortable) {
someNode.sort();
}
@mintern wrote:
Note that the *Entry classes are not easily accessible, so
instanceof
can't be used.
What do you mean by "not easily accessible"?
@shelby3 why are you commenting on issues that have been closed for 18 months including picking out comments made nearly two years ago? If you have an issue, it would be far more productive to state it against the current state of TypeScript instead of spending energy trying to debate things that have been dead a buried for extended periods of time.
@kitsonk wrote:
why are you commenting on issues that have been closed for 18 months including picking out comments made nearly two years ago?
Because the FAQ tells me to do precisely that:
Denial: It's healthy to believe that a suggestion might come back later. Do keep leaving feedback! We look at all comments on all issues - closed or otherwise. If you encounter a problem that would have been addressed by a suggestion, leave a comment explaning what you were doing and how you could have had a better experience. Having a record of these use cases helps us reprioritize.
@kitsonk wrote:
If you have an issue, it would be far more productive _to state it against the current state of TypeScript_ instead of spending energy trying to debate things that have been dead a buried for extended periods of time.
The bolded is exactly what I did. A bad design decision is never buried (click that link!). In particular, I am hoping Google SoundScript team is reading, so they will know which mistakes to consider avoiding.
And again, I asked for someone to provide to me justification for this feature change. Until I understand some counter logic to my points, I am going to continue to think it was a bad decision, hope it gets deprecated, and also try to influence it not being duplicated in other derivative works.
I am also open to being convinced via discussion that I am wrong, which I stated.
Btw, 1.6 was only released within the last year. The TypeScript development is highly accelerated at this time, and there is great danger of rushing design mistakes.
Btw, are you angry? I will quote from that same FAQ:
Anger: Don't be angry.
The way you worded your response to me was to entirely ignore the substance of my discussion and basically it seems you are trying to demoralize me by belittling my sincere and thoughtful contribution. A more productive (and less politically contentious) response from you would have actually address the substance of my comments. Any way, I will continue to do what I do, which is operate with a high standard of excellence and ethics and ignore those who try to turn factual discussions into political contests.
I also sense there will be some embarrassment if I am correct, which might be motivating the nature of your response. C'est la vie. All of us make mistakes, including myself. Being factual and objective is what matters. When the decision is inherently subjective, then I must acquiesce.
@shelby3 I am trying to point out that your thoughtful comments will simply be lost and ignored. Even though the FAQ states that, I think the context of which your comments were directed is for an issue that was to introduce user defined type guards. You seem to want to debate the nature of the nominal typing in TypeScript. I noticed you commented on #202 which is likely more aligned to the topic at hand, than some issue that has been resolved.
It was not politically motivated at all, nor am I angry. I saw someone who appears to be relatively new to TypeScript add significant comments to several older issues, some of which didn't seem to be aligned to the topic at hand. While maybe a bit direct, I was suggestion ways that individual might get more appropriate consideration to their thoughts.
@kitsonk thanks for clarifying that you are not waging any political campaign against me. To clarify the way I operate, I respond and document my own thoughts in context where they apply, for my own record of my thoughts and design decisions. If my sharing benefits anyone else and leads to any mutually beneficial discussion, then great. But driving action directly from my effort to study issues and comment is not the sole objective of the way I am working. For example, I will cross-reference in newer discussions, comments on older issues that I've read and commented on to try to bring myself up to speed on TypeScript. So just because a comment goes here in context, doesn't it mean it won't get tied into discussion impacting new discussion. Yes I am very interested in the nominal typing issue and in particular typeclasses and probably about to make a proposal on add them to model the prototype chain (but this is very complex). I did add an issue today suggesting if-else
and switch
as expressions, which I just noticed you are commenting negatively on. I am all over the place on TypeScript for the past 36 hours or so.
Just to set expectations here, there is absolutely no way we're going to _remove_ an oft-requested and popular type system feature from the language because some people feel it can't be used correctly. You can set up a TSLint rule to disallow it in your codebase if you think it's somehow dangerous.
We use type guards internally all over the place, e.g. https://github.com/Microsoft/TypeScript/blob/master/src/compiler/utilities.ts#L894 . There are lots of situations where they're useful and we added this because sometimes the built-in type checking things are not good enough.
@RyanCavanaugh wrote:
You can set up a TSLint rule to disallow it in your codebase if you think it's somehow dangerous.
Thank you for making me aware of that tool. Kudos on providing such a configurable toolset.
there is absolutely no way we're going to _remove_ an oft-requested and popular type system feature from the language because some people _feel it can't be used correctly_.
I appreciate your bluntness.
And it is good we air our respective mindsets, so we can see this isn't a personal battle, but somehow a difference of perspective.
There are no _feelings_ involved in my _objective_ analysis of the inability to the use the feature correctly. I would love to see someone refute my logic.
I explained that:
It is unfathomable to me that you (and the majority here) would think that depending on users to manually check the consistency of their declarations is acceptable for compiler-enforced type checking. Whether it is popular and vested inertia, is irrelevant to discussion about correctness (since correctness and applicability to scaling large projects is the entire point of TypeScript ... well maybe correctness and permissiveness?[1]). For derivative projects (including forks), might want to correct this mistake if the TypeScript is unwilling to. It just boggles my mind how this feature ever got into the language. I must be missing something and I am hoping someone can point out my myopia? I hope I am wrong, because my current state-of-mind is bewilderment as to how a team can produce such a great project with many wise decisions and let this very bad one slip through the cracks.
It really boggles my _objective_ mind. I am still hoping someone can point out how I am missing the point of this feature. I'd much prefer to find I am the one who is wrong, because it would increase my confidence in the TypeScript design team. Or at least admission of mistakes would be better than, "if it is popular, then correctness doesn't matter". Even some caveat in the documentation of the feature which explains how it is a potential soundness hole, would be more objective than covering our eyes and ears.
[1] Note I am preparing a rebuttal to those linked N4JS claims of being improvements over TypeScript and will link it here when I am done. And here it is.
@shelby3
The following works and there is no unchecked typing assumption:
abstract class Sortable {
abstract sort(): void
}class BaseCollection {
}class List extends BaseCollection implements Sortable {
sort() {}
}var someNode : BaseCollection | List
if(someNode instanceof Sortable) {
someNode.sort();
}
It does not work. Checkout the generated JavaScript and the behavior of the code as demonstrated here http://www.typescriptlang.org/play/#src=abstract%20class%20Sortable%20%7B%0D%0A%20%20abstract%20sort()%3A%20void%0D%0A%7D%0D%0A%0D%0Aclass%20BaseCollection%20%7B%0D%0A%7D%0D%0A%0D%0Aclass%20List%20extends%20BaseCollection%20implements%20Sortable%20%7B%0D%0A%20%20sort()%20%7B%7D%0D%0A%7D%0D%0A%0D%0Avar%20someNode%20%3A%20BaseCollection%20%7C%20List%0D%0A%0D%0Aif(someNode%20instanceof%20Sortable)%20%7B%0D%0A%20%20alert('someNode%20has%20Sortable%20in%20its%20prototype%20chain')%3B%0D%0A%7D%20else%20%7B%0D%0A%09alert%20('someNode%20does%20NOT%20have%20Sortable%20in%20its%20prototype%20chain')%0D%0A%7D
This however, works quite well.
abstract class sortable {
abstract sort(): void;
}
class BaseCollection {
}
class List extends BaseCollection implements Sortable {
sort() {}
}
var someNode : BaseCollection | List
function isSortable(collection: BaseCollection): collection is Sortable {
return typeof collection.sort === 'function';
}
if(isSortable(someNode)) {
someNode.sort();
}
It is my strong feeling that this example demonstrates the usefulness of user defined type guards.
There are no feelings involved in my objective analysis of the inability to the use the feature correctly. I would love to see someone refute my logic.
You are begging the question.
@aluanhaddad I was missing the construction of an instance for someNode
:
var someNode : BaseCollection | List // someNode is `undefined`
if(someNode instanceof Sortable) {
someNode.sort();
}
My corrected example shows that 100% abstract
classes can be put into the prototype
chain by employing the extends
keyword, which was the intended point I was making.
The remaining problem is that TypeScript does not support multiple inheritance of subclass through linearization of the hierarchy, so we can't currently extend
from both BaseCollection
and Sortable
simultaneously. But that shouldn't be necessarily...
(P.S. I have never coded in TypeScript and only started to learn it 48 hours ago. That will give you some indication of my experience level in general with programming and languages)
It appears to me to be an error that TypeScript is not putting the implemented interfaces in the prototype
chain so that instanceof
will work correctly for interfaces. Even though (even abstract
) classes are treated nominally for the prototype
chain with extend
, they are still structurally typed otherwise. So the argument that interfaces are structurally typed doesn't seem to be a contradiction. And since interfaces have no implementation, they don't need linearization (they can be placed in the prototype
chain in any random order). Note I need to search the issues and reread the FAQ to see if this has already been discussed.
https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#4.24
A type guard of the form x instanceof C, where x is not of type Any, C is of a subtype of the global type 'Function', and C has a property named 'prototype'
- when true, narrows the type of x to the type of the 'prototype' property in C provided it is a subtype of the type of x
So I maintain my stance that the feature of this issue was not necessary. And now I add a bug report that TypeScript is failing to put interfaces into prototype
chain. Not fixing a bug and making an unnecessary unsound typing feature to compensate for not doing so, is a design mistake. That is what open source is for, so that with enough eyeballs someone will catch the error.
It is my strong feeling that this example demonstrates the usefulness of user defined type guards.
There are no feelings involved in my objective analysis of the inability to the use the feature correctly. I would love to see someone refute my logic.
You are begging the question.
Again feelings have nothing to do with my posting activity. I am here on mission of production, documentation, sharing, and objectivity.
I realize you are a younger man well before the precipitous drop in testosterone (starting university in 2007 versus mine in 1983) and I notice you are upvoting spiteful comments against me in other issue threads where I am participating, but really man I am 51 years old. Ego is for little people. You'll earn your stripes eventually and realize the wisdom of the linked blog.
I didn't react strongly to this issue feature because of any desire to prove someone is wrong on the Internet, bravado, or offended by someone else's ego. I reacted strongly because the design of TypeScript is (potentially) a serious matter of great importance to work at hand.
Btw, I appreciate your discussion (even the humorous unnecessary bravado part). You are helping me (we are helping each other) to understand the solution. Isn't that wonderful.
@shelby3 wrote:
Even though (even
abstract
) classes are treated nominally for theprototype
chain withextend
, they are still structurally typed otherwise. So the argument that interfaces are structurally typed doesn't seem to be a contradiction.
I suppose the argument against my logic would be that putting interfaces in the prototype
chain would be metadata inserted by the compiler and not an erasure of all elided typing hints.
The feature of this issue (which I argue is unsound) burdens the user with inserting superfluous adhoc metadata and eliminates the ability of the compiler to check it. Thus on balance, I think my logic stands. The compiler should insert the metadata and thus insure soundness.
Here's a perspective from a huge fan of TypeScript who is not involved in its development. We had a huge JavaScript codebase that we converted to TypeScript, and it's now much easier to work with. We now have easier and safer refactoring, auto-completion, semantic navigation, and some compiler checks on annotations that used to be part of comments instead of being part of the code.
@shelby3 wrote:
Afaics, the compiler can't check the soundness of the declaration, thus (if my analysis is correct?) it is impossible to use the feature correctly. What is the point of a compile-time check on types, if type declarations can't be checked by the compiler to be consistent?
Because of the incompleteness theorem, there is always a correct program that cannot be described by a rigid type system. Every language that I know of has an escape hatch: Rust has unsafe
, Java has reflection and type-casting, and the list goes on and on. More to the point, raw JavaScript is completely dynamic, and it's up to the programmer to write everything correctly, with no help from a compiler.
To me, TypeScript is a tool that helps me write correct programs. Its gradual typing is a hint about its philosophy: start with whatever you have, and improve it incrementally by adding information to your program. As you continue to do that, TypeScript helps you more and more. I don't think its goal is to be perfect, just to be better, and it has succeeded for us at least: the TypeScript is significantly better than the JavaScript it replaced for us.
Bringing it back to this discussion, user-defined type guards are a perfect example. I can write a little function (like aluanhaddad's isSortable
example) that tells the compiler, "Hey, I know that you can't tell this function is actually checking for a Sortable
type, but trust me, I know what I'm doing here." And the compiler will believe me and allow me to use this new isSortable
tool in the rest of my program.
Without that feature, I have to write the check and cast anywhere in my program that I need to check for a Sortable
type, and that's much more error-prone than a user-defined type guard.
In other words, you're right that the compiler isn't verifying the correctness of a user-defined type guard function, but I'm pretty sure that's exactly the point of the feature.
And this response is from someone who is potentially going to become a huge fan of TypeScript.
Note in following I am not criticizing you. Not at all, I appreciate you raising this perspective so I can respond with my perspective on it. It isn't personal at all. We are trying to learn from each other. I am open to the possibility of learning that my perspective is incorrect.
@mintern wrote:
Every language that I know of has an escape hatch:
But my point is that the feature added from this issue is not an escape hatch from the compiler. A feature was added to the compiler to check something that it would not otherwise not check, and we are enabling a human to tell the compiler what the correct type of that check should be without enabling the compiler to have any way to verify that the human didn't make a mistake. A compiler is normally to enforce the consistency of invariants it derives from declarations (a declaration can never be a mistake because it is either consistent or a compiler error), not accepting mistakes from humans as a foundation for consistency. Rather just leave the checking off and have a true escape hatch, e.g. the type any
, if the compiler can't check it.
I understand that any escape hatch makes the compiler's internal consistency potentially inconsistent externally, but the point of a compiler is never to be consistent with the entire universe, but to maintain an internal consistency. I am arguing we are destroying the internal consistency.
Also my point is that the compiler could indeed check the motivating cases properly but isn't doing, so instead we create an unnecessary feature which is more unsound than not having the feature at all and leaving it uncheck (an unverified check is worse than no check, because it is an assurance that be totally deceiving).
"Hey, I know that you can't tell this function is actually checking for a Sortable type, but trust me, I know what I'm doing here."
That is a slippery slope that I will run to hills away from TypeScript if that becomes the accepted community principle on compiler design. And I will actively deride TypeScript for that attitude if it is indeed prevalent (unless someone can teach/convince me it is a correct principle of design). That quoted English statement (not this issue feature) is perfectly okay to do within an escape hatch, i.e. that was the traditional theme of JavaScript (loose and free).
It is perfectly okay to create an escape hatch where we acknowledge that the compiler is not checking. IMO, it is not okay to start fooling the compiler to make assurances which rely on human error. AFAICS, that defeats the entire point of compile-time checking.
If we build a house on quicksand, we'll eventually end up with a clusterfuck. Let's not conflate escape hatches with houses-of-mirrors. The analogy to house-of-mirrors is very intentional. Soon we won't know what we are looking it, as the confluence and contagion of human error input to the compiler combines with other features in unfathomable Complexity of Whoops™. Due to the Second Law Of Thermodynamics (disorder always trending to maximum), we are most assuredly to end up with no typing assurance eventually any where in the code. Defending order requires a continual work to ward off erosion.
Perhaps @ahejlsberg has accumulated some experience which says otherwise? (Btw Anders do you remember Jeff Stock from Borland?)
Without that feature, I have to write the check and cast anywhere in my program that I need to check for a
Sortable
type, and that's much more error-prone than a user-defined type guard.
Let's bite the bullet and have the compiler add interfaces to the prototype
chain, so that instanceof
works as expected.
K.I.S.S. applies here.
Building up a clusterfuck of complexity just because of some purist design goal to never add metadata for erased typing hints is bogus, because we are adding metadata to the prototype
chain for the base classes which may be erased if never instantiated.
@shelby3
I realize you are a younger man well before the precipitous drop in testosterone (starting university in 2007 versus mine in 1983) and I notice you are upvoting spiteful comments against me in other issue threads where I am participating, but really man I am 51 years old. Ego is for little people. You'll earn your stripes eventually and realize the wisdom of the linked blog.
These statements have no place in this discussion.
Btw, I appreciate your discussion (even the humorous unnecessary bravado part). You are helping me (we are helping each other) to understand the solution. Isn't that wonderful.
Indeed, I was attempting to be humorous, but I was also being serious. Stating that one is objective and not coming from any emotional place, especially after having already intimated that others are coming from an emotional place, makes one's further statements highly suspect.
Yes, we are helping each other learn, and yes that is a wonderful thing.
So I maintain my stance that the feature of this issue was not necessary. And now I add a bug report that TypeScript is failing to put interfaces into prototype chain. Not fixing a bug and making an unnecessary unsound typing feature to compensate for not doing so, is a design mistake. That is what open source is for, so that with enough eyeballs someone will catch the error.
The remaining problem is that TypeScript does not support multiple inheritance of subclass through linearization of the hierarchy, so we can't currently extend from both BaseCollection and Sortable simultaneously.
In TypeScript interfaces have always been structural. They declare a compile time name for a certain shape. Not all TypeScript/JavaScript programs are class based. Consider the following code.
export function openModal(modalOptions: ModalOptions): void {
const modal = createModalImpl(modalOptions.template, modalOptions.viewModel);
modal.onOpen = modalOptions.onOpen;
modal.onClose = modalOptions.onClose;
modal.editable = modalOptions.readonly;
modal.open();
}
export interface ModalOptions {
template: string;
viewModel: any;
onOpen: (callback: () => void);
onClose: (callback: () => void);
readonly: boolean;
}
The purpose of the ModalOptions interface is to declare part of the interface of the _function_ openModal
so that the TypeScript compiler can check that the correct arguments are specified and so that the caller knows what to pass. It is used for both documentation and typechecking to formally describe the contract of the API, no classes are involved. Thus, the following two examples have the same behavior.
import { openModal, ModalOptions } from 'basic-modal-utilities';
import template from './template.html';
// ex 1.
const vm = {
name: 'Bob',
friends: ['Alice', 'Eve']
};
openModal({
template: template,
viewModel: vm,
readonly: true,
onOpen: {
console.log(`modal opened with data ${json.stringify(vm)}.`);
},
onClose: () => {
if (vm.name !== 'Bob' ||
vm.friends[0] !== 'Alice' ||
vm.friends[1] !== 'Eve' ||
vm.length > 2) {
console.error('Test failed: viewModel was modified');
}
}
});
// ex 2.
class ReadonlyModalOptions implements ModalOptions {
readonly = true;
constructor(public viewModel, public template) { }
onOpen() {
console.log(`modal opened with data ${json.stringify(this.viewModel)}.`);
}
onClose: () => {
if (this.viewModel.name !== 'Bob' ||
this.viewModel.friends[0] !== 'Alice' ||
this.viewModel.friends[1] !== 'Eve' ||
this.viewModel.length > 2) {
console.error('Test failed: viewModel was modified');
}
}
}
const vm = {
name: 'Bob',
friends: ['Alice', 'Eve']
};
openModal(new ReadonlyModalOptions(vm, template));
Lots of APIs work this way. They simply require that a caller pass arguments that meets their requirements, but not how those requirements are defined. This is not only a common pattern but a useful one. If interfaces were added to the prototype chain, then it would be reasonable for someone to add the following line of code to the openModal
function:
if (!modalOptions instanceof ModalOptions) {
throw TypeError();
}
This introduces serious problems. Ex 1. no is longer valid. This is (annoying, but not fatal) for TypeScript consumers who do not want to create a class, but it is catastrophic for JavaScript consumers that can not implement the interface without manually wiring up the prototype chain themselves.
Another issue with linearization is that, as more interfaces are implemented, the depth of the synthetic class hierarchy grows exponentially. This becomes especially problematic in cases where interfaces extend other interfaces and classes reimplement interfaces.
Consider
interface Sortable<T> {
sort<U>(sortBy: (x: T) => U): this;
}
interface NumericallyIndexable<T> {
[index: number]: T;
}
interface ArrayLike<T> extends Sortable<T>, NumericallyIndexable<T> {
map<U>(projection: (x: T) => U): ArrayLike<U>;
filter(predicate: (x: T) => boolean): ArrayLike<T>;
}
interface Grouping<K, V> {
get(key: K): ArrayLike<V>;
}
class RichCollection implements ArrayLike<T>, Sortable<T> {
constructor(private elements: T[] = []) {
elements.forEach((element, index) => {
this[index] = element;
});
}
sort<U>(sortBy: (x: T) => U) {
return new RichCollection(
this.elements.sort((x, y) => x.toString().compare(y.toString())
);
}
map<U>(projection: (x: T) => U) {
return new RichCollection(this.elements.map(projection));
}
filter(predicate: (x: T) => boolean) {
return new RichCollection(this.elements.filter(predicate));
}
groupBy<K>(keySelector: (x: T) => K): Grouping<K, T> { ... }
}
@aluanhaddad wrote:
These statements have no place in this discussion.
Neither do these provocateurs:
It is my strong feeling that this example demonstrates the usefulness of user defined type guards.
There are no feelings involved in my objective analysis of the inability to the use the feature correctly. I would love to see someone refute my logic.
You are begging the question.
When you intentionally start a fight and promote acrimony, then don't be surprised nor offended when you get punched. Sort of logical how that works eh? Reminds of George Carlin's statement about those who in Hawaii who build their homes next to an active volcano and then are surprised when they have lava flowing through the living room.
especially after having already intimated that others are coming from an emotional place
Listen son, I don't know what your problem is, but before you make a false accusation, please remember to quote the source of your false accusation. I made no such precedent statement about others. I quoted from the FAQ. Direct any such accusation to the source.
You are making trouble. Please stay on point and leave the personal noise out of the technical discussions.
I hope this is the end of this non-technical noise that nobody wants to have to wade through. (I know it won't be the end. I know you will carry this grudge around endlessly and in the future I'll have to deal with you taking out your repressed desire to spank me. I've been on the Internet long enough to know how this works.)
@shelby3
Listen son, I don't know what your problem is, but before you make a false accusation, please remember to quote the source of your false accusation. I made no such precedent statement about others. I quoted from the FAQ. Direct any such accusation to the source.
In https://github.com/Microsoft/TypeScript/issues/1007#issuecomment-246127188
you wrote
Btw, are you angry? I will quote from that same FAQ:
suggesting that @kitsonk was angry. There was no evidence for that. Even if there were, you were asking the question rhetorically, which weakens your argument by introducing an ad hominem fallacy.
@shelby3 You are wrong. 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.
Its not a type system that ensures correctness. Its a type system that works with the user to ensure it does.
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.
@aluanhaddad:
Btw, are you angry? I will quote from that same FAQ:
Do you not see question mark? Are you just determined to ignore the difference in the English language between a question and an implication or accusation or even insinuation.
Even if there were, you were asking the question rhetorically, which weakens your argument by introducing an ad hominem fallacy.
Where is your proof that I was asking the question rhetorically? You make an assumption that I am evil because obviously that is what you want to believe (since there is no hard evidence to base your accusation). Fantasy is nice and easy. Evidence and objectivity require one to be more circumspect.
There was no evidence for that.
Perhaps you should remember to read the entire comment:
@kitsonk wrote:
why are you commenting on issues that have been closed for 18 months including picking out comments made nearly two years ago?
...
Btw, are you angry? I will quote from that same FAQ:
Anger: Don't be angry.
And the follow-up:
@kitsonk thanks for clarifying that you are not waging any political campaign against me. To clarify the way I operate, I respond and document...
Why are you wasting precious time and carrying around baggage. Let it go and focus on the technical discussion. I will not respond again on this off-topic noise.
@shelby3 I do not think ageist or sexists remarks have any place in a technical discussion. I felt compelled to object.
Regardless, perhaps you would care to respond to the technical side of my comment, specifically the concerns I raised about linearization of interfaces into the class hierarchy.
@aluanhaddad:
In TypeScript interfaces have always been structural.
...if (!modalOptions instanceof ModalOptions) { throw TypeError(); }
If the API consumes structural type, then it won't do that. If it does that, then it intends to consume nominally. The API designer hasn't lost any control. For maximum interoption with non-TypeScript transpiled consumers, the API design may choose to not to use nominal typing.
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.
Another issue with linearization is that, as more interfaces are implemented, the depth of the synthetic class hierarchy grows exponentially.
The entire prototype chain will only be searched for instanceof
and missing properties (unless there are any Object properties needed, but this could be copied forward in the chain as an optimization). EMCAScript can choose to provide further optimizations in their standard if they realize that empty prototype objects that only exist to service the instanceof
operator could be better served with a Map
. And TypeScript could use a Map
for optimization interim, while also populating the prototype
chain for interoperability.
Inability to optimize is not usually a winning argument.
@aluanhaddad:
@shelby3 I do not think ageist or sexists remarks have any place in a technical discussion.
OMG! He pulls out the political correctness legal discrimination and State-monopoly-on-force weapon. I see you want to build a political and/or legal case for a ban or what ever. (I didn't expect it to escalate this far, so this is an eye opener for me when dealing with the latest crop of State indoctrinates)
I will have no more discussion nor interaction with you. You just earned a ban (block) from me, so will no longer see your comments. You may speak to my attorney.
Edit: unfortunately the Github block feature doesn't hide your comments from me. Nevertheless I am putting you on notice that I wish to stop all communication with you. Thank you.
@spion wrote:
@shelby3 You are wrong.
Wrong about what? Please quote my statement that is "wrong". Rather I think you mean to discuss and argue is your interpretation that TypeScript has design goals which don't match my concerns. That doesn't make me wrong. You know very well from other thread where you are participating, that I have stated that TypeScript's goals seem somewhat ambiguous to me.
The false accusations are flying in and now you @spion are downvoting my comments. So I see it has turned into a "shoot the messenger" politics instead of carefully thought out non-acriminous discussion of technology. That means it is digressing from technical discussion into a pissing match, and thus it is about time for me to leave unless the level of maturity and ethics rises pronto.
No it hasn't. You started to veer off the technical rail by discussing age and testosterone, then responded with "Let it go and focus on the technical discussion. I will not respond again on this off-topic noise". I down-voted only these two comments because they are hypocritical.
You are inconsistent, rude and abrasive. Hell, you just sliced my comment at the "you are wrong" point and proceeded to ignore the rest of it, pretending it doesn't exist, and went back to your diatribe against this feature.
I suggest you use TypeScript for at a couple of weeks before commenting further on it.
@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.
@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.
@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).
@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.
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.
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).
@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 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?
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.
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.
@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.
@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.
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?
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:
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.
@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.
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.
@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.
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.
@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.
@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.
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.
@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 forinstanceof
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 instanceof
for 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?
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?
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.
@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.
}
@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.
@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.
@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.
@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
.
@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.
@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.
@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.
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) {
}
This is not a support forum.
Questions should be asked at StackOverflow or on Gitter.im.
@nmaro it has been suggested but not implemented so far. See for example #8655, and other issues linked from there.
Most helpful comment
Here's a perspective from a huge fan of TypeScript who is not involved in its development. We had a huge JavaScript codebase that we converted to TypeScript, and it's now much easier to work with. We now have easier and safer refactoring, auto-completion, semantic navigation, and some compiler checks on annotations that used to be part of comments instead of being part of the code.
@shelby3 wrote:
Because of the incompleteness theorem, there is always a correct program that cannot be described by a rigid type system. Every language that I know of has an escape hatch: Rust has
unsafe
, Java has reflection and type-casting, and the list goes on and on. More to the point, raw JavaScript is completely dynamic, and it's up to the programmer to write everything correctly, with no help from a compiler.To me, TypeScript is a tool that helps me write correct programs. Its gradual typing is a hint about its philosophy: start with whatever you have, and improve it incrementally by adding information to your program. As you continue to do that, TypeScript helps you more and more. I don't think its goal is to be perfect, just to be better, and it has succeeded for us at least: the TypeScript is significantly better than the JavaScript it replaced for us.
Bringing it back to this discussion, user-defined type guards are a perfect example. I can write a little function (like aluanhaddad's
isSortable
example) that tells the compiler, "Hey, I know that you can't tell this function is actually checking for aSortable
type, but trust me, I know what I'm doing here." And the compiler will believe me and allow me to use this newisSortable
tool in the rest of my program.Without that feature, I have to write the check and cast anywhere in my program that I need to check for a
Sortable
type, and that's much more error-prone than a user-defined type guard.In other words, you're right that the compiler isn't verifying the correctness of a user-defined type guard function, but I'm pretty sure that's exactly the point of the feature.