-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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 predicate types #34
Comments
Certainly we could hardcode this stuff in the typechecker, but I would love to come up with syntax to declare this kind of knowledge. We performed this kind of thought experiment with Hack without much success. So for an
But that's kind of verbose. Maybe overloading can help?
Ideally you'd also be able to declare when certain functions will throw, like in an
|
I just make problems; I don't fix them. :) I agree this probably isn't super trivial to fix. |
The way this is tackled in things like core.typed is through a shorthand syntax for predicates on types. For example
Where 0 refers to the first argument (x), and specifies refinements in each branch based on the return value. I don't think you'd ever get annotations like this to be particularly clean, though making them available certainly reduces the amount of special casing/privileged forms. (I'm not really sure how you'd translate this kind of syntax to Flow, though having functions from types-to-types would seem to be valuable, so that declare |
TypeScript developers and users are currently discussing a similar feature in issue 1007 |
On a related note, since:
constraining types with code such as:
is not sufficient, whereas a simple I was actually really surprised Flow didn't catch the null issue I had in code such as the above, and only found it during testing. |
It looks like TypeScript ultimately decided on predicate function syntax, should flow try to use the same syntax? function isB(p1: any): p1 is B;
(p1: any) => p1 is B; Where the syntax in the return type could be Where |
@leebyron Cool! Can you provide a link to this discussion on TypeScript's side of things? It's possible that Flow might have different capabilities and constraints around refinements that could affect the syntax here. For example, refinements can be anded and ored together, and flow can refine specific keys within object types. Also, it seems like a function would always carry a (maybe void) return type, and optionally a refinement. Not either/or. For example, |
microsoft/TypeScript#1007 is the issue discussing this, implemented in microsoft/TypeScript#3262. https://github.com/Microsoft/TypeScript/pull/3262/files#diff-b09312f5c227031dfe4692c34364e2f0R16 this file is full of parser test cases. |
TypeScript's behavior seems to be that functions which return these type predicate refinements actually always return booleans. I would propose that within the function, flow actually checks to ensure any possible "true" return agrees with the annotated type refinement. Example, this should typecheck: function isFunction(value: any): value is Function {
if (typeof value === 'function') {
return true;
}
return false;
}
var value: any = () => {};
if (isFunction(value)) {
value();
} This should fail: function isFunction(value: any): value is Function {
if (typeof value === 'function') {
return true;
}
// Error: isFunction must return boolean
}
var value: any = () => {};
if (isFunction(value)) {
value();
} And this should fail: function isFunction(value: any): value is Function {
return true; // flow doesn't think "value" is Function here.
}
var value: any = () => {};
if (isFunction(value)) {
value();
} |
Yeah, it seems like the This system is a good 80% solution, I think. We wouldn't be able to annotate the type of I agree that flow should check that the return type is boolean, and it should also check that the truthiness of the returned value entails the correct refinement. So, this would be bad: function isFunction(x: any): x is Function {
if (typeof value === 'function') {
return false; // backwards
}
return true;
} |
Totally, flow should catch that backwards predicate.
Yeah, I think this is okay too. Invariant does something a little bit different anyhow, it may make sense to be a different scope of work.
flow should be able to type check this correctly as well. |
Ah, it would also be very cool if we could infer a proof-carrying boolean. Consider this trivial example. function isNull(val) { return val == null }
function safeFormatNumber(x: ?number) {
if (isNull(x)) {
return "";
} else {
return x.toFixed(2); // no error
}
} We didn't annotate |
That's a great point. I imagine that would work similarly to flow's current return type annotation rules. If your |
I'm still interested in this feature, and it'd be great if @gabelevi could chime in about whether we want to move forward with this syntax. If we do, we'd need to build in support with Babel to strip the type. Currently |
It would be interesting if there were some way for the refinements to be associated across multiple functions. For example a has/get scenario: class Wrapper<V> {
...
get(): ?V {
return this._v;
}
has(): boolean {
return this._v != null;
}
} unwrap(wrapper: Wrapper<string>): string {
return wrapper.has() ? wrapper.get() : 'default_value';
} |
For me will be nice following syntax: function isNeededThing(aaa: predicate): boolean {
if(...) {
aaa: SomeType = aaa;
return true;
}
return false;
} |
Right now at work and I can give more clarification. More complex example: function isNeededThing(testObject: some<T1 | T2 | T3>): boolean {
if(...) {
testObject: T2 = testObject;
return true;
} else if (...) {
testObject: T1 = testObject;
return true;
}
testObject: T3 = testObject;
}
if (isNeededThing(someObj)) {
// here someObj may became one of the types T1 | T2
} else {
// here someObj became T3
} I offer to add In generally And Another example, only with const callbackList: FunctionT[] = [];
if (isFunction(cb)) {
cb: FunctionT = cb;
callbackList.push(cb);
} |
@nodkz If I'm understanding your examples right, then I don't think that idea adds anything that the function isNeededThing(testObject: T1|T2|T3): testObject is T1|T2 {
return testObject instanceof T1 || testObject instanceof T2;
}
if (isNeededThing(someObj)) {
// here someObj is T1 | T2
} else {
// here someObj is T3
} Also consider that your way can't have the function be described from a declaration, unlike the declare function isNeededThing(testObject: some<T1 | T2 | T3>): boolean; |
I really like this proposal, and read all the comments but may not understand everything, so apologies for stupid things in advance 😁 Best would ofcourse be to have it infer it automatically, but if that doesn't always work out (and for other things like exits or errors?), what about making special wrapper types for that that work with Tagged Unions?
where TypeCast's definition would be something like This would also make it (maybe something-ish) possible to have invariant working like
although I am not sure if the And process.exit would be
EDIT: EDIT EDIT:
|
Agree, but I think it's my responsibility to tell Flow developes about such cases (when I can't use Flow).
I can declare types for 3-rd party API and Flow will understand it perfectly
Sure, there's no way right now. But, maybe, ideas like
can help us in future.
Totally agree. I reopened my original issue. Everyone who want to discuss - welcome to #4057 |
Any progress here? |
@philipp-sorin |
@zaygraveyard this helps only with the most basic custom type predicates/refinement use-cases. What I believe this issue is really about is implementing something like what is called user-defined type guards in typescript, that is a way to define a type predicate // Pretend this is an `opaque type Email = string` imported from another module
class Email {}
// Here you really want to tell flow that `isEmail(x) === true` if and only if
// `x` is of type `Email`, and this is not what `%checks` currently does
function isEmail(x: mixed): boolean %checks {
return typeof x === 'string' && x.includes('@');
}
function spam(e: Email) {}
function main(s: string) {
if (isEmail(s)) {
spam(s);
// ERROR: `string` type is incompatible with the expected param type of `Email`
} else {
throw new TypeError(`Invalid email: ${s}`);
}
} EDIT: No doubt this was already mentioned above, but this comment makes it seem like this issue has been finally addressed. I actually jumped to such conclusion after reading it and was then disillusioned, so I had to reply to at least make it clear to others that this is not really the case. |
@futpib if you briefly look through the thread above, you could see that this idea was already mentioned. |
It seems like the The flow-typed definition might look like this:
which looks like a strong enough definition for flow to infer, but it isn't. I found the definitions for Line 278 in a610efd
and it looks like you special case it within the code. Wouldn't it be possible to look at the typed value and then infer the types in an if so we could do
without needing any syntax changes? |
@lukeapage See #4196 for why |
@lukeapage your isString(value: string): true;
isString(value: any): false; looks lovely. It's like another way to say |
what is the right way of doing this on Flow? |
There is no current way to write Regarding non-throwing predicate functions, refer to |
Going through some issue backlogs so I apologies if I'm closing this issue prematurely but I believe at least the original issue has been addressed. Specifically, function isFunction(a): boolean %checks {
return typeof a === 'function';
}
function concat(a: (() => null) | number) {
if (isFunction(a)) {
a();
}
} |
Not really fixed, because it's still impossible to write meaningful type-checking functions that don't or can't use
This encourages Here's a real-world example from React: https://overreacted.io/how-does-react-tell-a-class-from-a-function/
Ideally flow should have a way to type a |
@vicapow if such a function's body may contain only single expression and must be based on typeof that is not a resolution. The whole purpose of this feature is to indicate to Flow your specific type approaches, they may not rely on prototype chain or primitives. Like, for instance, I'm using Symbols for my types and they often have Other examples by @bluepnume in topic above. |
Agree, |
It would be great to see this implemented, hopefully we can get it soon. |
This also would allow to remove hardcoded invariant declare function invariant(cond: boolean, message: string): empty %asserts(cond) /cc @panagosg7 |
Just to continue documenting TypeScript's progress on this front, the recent release (3.7) includes an extension to the syntax they added in 2015 to include throwing functions like I'd still love to see both features in Flow! |
Would be nice to see this prioritised. |
We now have type guards |
Currently, if you do
then Flow knows that the code within the if statement is a function. This doesn't work if you instead call a helper function like Underscore's
_.isFunction
. Similarly, utilities likeObject.assign
andReact.PropTypes
seem to have hardcoded behavior at the AST level, so they don't work when aliased or given alternative names.Will it be possible to teach Flow about these sorts of functions so that it's possible to use these third-party libraries without changing code too much?
The text was updated successfully, but these errors were encountered: