Repository navigation
Support user-defined type guard functions #1007
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Oct 30, 2014 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
Reacted by shelby3RyanCavanaugh commented
on Oct 31, 2014 MemberAuthorMore actionsPerhaps the second example was not clear in its intent. Consider something like this, where
instanceofwould 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() { ... } }
Reacted by Aluan Haddad and Andreas HuberReacted by shelby3Indeed its intent is indeed clearer. So I have two comments :
- 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.
- 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. } }
Bart Verkoeijen (@bgever) We did indeed consider that. It breaks down to (a) allowing
x is TwhereTis 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[]; }
Arnav Singh (@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
objcould be:var obj: { id: number; description: string; } | any; if ('id' in obj) { // obj.id is valid here }
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 likehasOwnProperty(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.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,entryis aFileEntry, and whenentry.isDirectory,entryis aDirectoryEntry.Note that the
*Entryclasses are not easily accessible, soinstanceofcan't be used.66 remaining items
spion (@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
Thenablenominal interface, there needs to be a single value that represents it in the prototype chain (in order forinstanceofto work).If we are referring to subclassing and not typeclasses, the
Promiseconstructor function controls what will be put in theprototypechain. Even if you import multiple instances of aPromise, they will all for each use only oneThenableinterface perprototypechain. But with redundant imports, all of theseThenableinterfaces will not have the same reference in memory, since we'd have multiple instances of thePromiseconstructor function. So I agree that non-redundant imports are necessary if we expect to have a unifiedinstanceoffor all instances if we are basinginstanceofon 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
Promisealso 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 Arraymeans 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?
Reacted by Sean VieiraAs of ES6,
instanceofis decoupled from the prototype chain due toSymbol.hasInstance. Walking the prototype chain is now just the default behaviour. But inobj instanceof Obj, if theObjvalue has its own[Symbol.hasInstance]property, then that will determine howinstanceofbehaves.Reacted by Jacob Eggers, Kagami Sascha Rosylight, Sean Vieira, spion and shelby3shelby3 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 } }
Troy Gerwien (@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 (@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 sameprototypeand[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.
Jacob Eggers (@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
prototypechain for that suggestion.Troy Gerwien (@yortus) thank you.
Aluan Haddad (@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 thatinstanceofmay 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 interfacesisA(x), I would use a keyword likeimplementsorimplementationOf, maybe overloadinginstanceOf.Jacob Eggers (@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
Animaland another if it's one ofVehicle, you still need to structurally check which object it implements.Jacob Eggers (@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.
Reacted by Nick Redmark and Aluan Haddad- locked and limited conversation to collaborators
on Jun 18, 2018
We currently only support some baked-in forms of type checking for type guards --
typeofandinstanceof. 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 Twherexis a declared parameter in the signature, orthis, andTis 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 toexprthe same way thatexpr instanceof tandtypeof expr === 'literal'do today.