-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support user-defined type guard functions #1007
Comments
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 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 :
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.
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 |
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 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 var obj: { id: number; description: string; } | any;
if ('id' in obj) {
// obj.id is valid here
} |
Actually, I suppose 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 For your second example, I assume you meant Even if the obj was of type |
Sorry, perhaps I wasn't clear. I was agreeing with you that isArray should return 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 Note that the |
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 |
|
First, you can't populate the prototype chain with interfaces. This will not work because 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 As to why type guards are okay, I'll just quote myself without the "you are wrong" part:
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:
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.
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 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 Perhaps a compiler option would be to omit the runtime checks, then the programmer is confident their runtime environment is cordoned soundly. @spion wrote:
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:
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 Choose your poison.
Yeah I was aware of that from this, and thanks for citing that source which explains it more completely.
I'd need a more thorough explanation to understand how npm managed to break 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.
By 'this', I assume you are referring to employing JavaScript's I doubt very much that 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.
How is supporting a JavaScript feature only a TypeScript-only feature? Offering ways to type the 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.
What is the proven clash? And I don't think the goal is "existing JavaScript code" but rather "the existing ECMAScript standard".
In my code, I detect 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?
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? |
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 |
@spion wrote:
Please re-read my prior comment as I have rebutted this "normally" argument.
You are conflating nominal typing with subclassing. That is why I am preparing to promulgate typeclasses typing of the |
The difference in pitfalls is fundamental. With |
@spion wrote:
That is an interesting perspective, but it depends on who and what was "supposed to". If 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. |
Promises/A+ thenables are a good example. If you want to specify a 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. |
That would mean avoiding edit: removed problematic section. |
@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. 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:
If we are referring to subclassing and not typeclasses, the
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 A consistent importing system wide is important. I agree but not if the other possibilities mentioned above and below can work sufficiently well.
I understand there are broken legacy realms. C'est la vie. We move forward anyway.
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? |
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 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
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, |
@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:
I did not define 'realms'.
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 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.
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.
What is the problem you envision? If the module for the
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 @yortus thank you. |
@aluanhaddad wrote:
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 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 |
@eggers wrote:
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 |
@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:
|
This is not a support forum. Questions should be asked at StackOverflow or on Gitter.im. |
We currently only support some baked-in forms of type checking for type guards --
typeof
andinstanceof
. 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
wherex
is a declared parameter in the signature, orthis
, andT
is any type. This type is actually considered asboolean
, but "lights up" type guards. Examples:The forms
if(userCheck([other args,] expr [, other args])) {
andif(expr.userCheck([any args]))
would apply the type guard toexpr
the same way thatexpr instanceof t
andtypeof expr === 'literal'
do today.The text was updated successfully, but these errors were encountered: