Skip to content

Support predicate types #34

Closed
Closed

Description

@sophiebits

Currently, if you do

if (typeof x === "function") {
  // ...
}

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 like Object.assign and React.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?

Activity

  1. gabelevi commented on Nov 18, 2014

    @gabelevi
    Contributor

    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 isFunction(x) test, it would be great if we could declare that, if isFunction(x) returns true, then x is a function. Otherwise it is not a function. So maybe something like

    declare function isFunction<T>(T x): true when T = function, false otherwise;
    

    But that's kind of verbose. Maybe overloading can help?

    declare function isFunction(x: function): true;
    declare function isFunction(x: not function): false;
    

    Ideally you'd also be able to declare when certain functions will throw, like in an assertFunction(x) function

    declare function assertFunction<T extends function>(x: T): T;
    declare function assertFunction(x: not function): explodes;
    
  2. sophiebits commented on Nov 18, 2014

    @sophiebits
    ContributorAuthor

    I just make problems; I don't fix them. :) I agree this probably isn't super trivial to fix.

  3. c-spencer commented on Nov 19, 2014

    @c-spencer

    The way this is tackled in things like core.typed is through a shorthand syntax for predicates on types. For example (ann function? (Pred Function)) defines a boolean return type with type refinement implications for the function? declaration. The full annotation would be along the lines of:

    (ann function? (All [x] (IFn [x -> Boolean
                                       :filters {:then (is Function 0) :else (! Function 0)}])))
    

    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 function isFunction<T>(T x): Predicate(function); could return the properly annotated Boolean return type)

  4. spion commented on Nov 20, 2014

    @spion

    TypeScript developers and users are currently discussing a similar feature in issue 1007

  5. jareware commented on Dec 16, 2014

    @jareware

    On a related note, since:

    typeof null === 'object'
    

    constraining types with code such as:

    if (typeof o === 'object') {
        o.someProperty;
    }
    

    is not sufficient, whereas a simple _.isObject(o) would be.

    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.

  6. leebyron commented on Jul 30, 2015

    @leebyron
    Contributor

    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 Type | Predicate

    Where Predicate := Identifier "is" Type

  7. samwgoldman commented on Jul 31, 2015

    @samwgoldman
    Member

    @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, _.isFunction(x) still returns a boolean, but if the return value is true, x is an array. If it's false, x is not an array, and flow can use that information, too.

  8. leebyron commented on Jul 31, 2015

    @leebyron
    Contributor
  9. leebyron commented on Jul 31, 2015

    @leebyron
    Contributor

    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();
    }
  10. samwgoldman commented on Jul 31, 2015

    @samwgoldman
    Member

    Yeah, it seems like the x is y syntax is a boolean type with additional information (predicates that hold when the boolean is true).

    This system is a good 80% solution, I think. We wouldn't be able to annotate the type of invariant, for example, because it returns void or blows up. I think that's OK.

    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;
    }
  11. leebyron commented on Jul 31, 2015

    @leebyron
    Contributor

    Totally, flow should catch that backwards predicate.

    This system is a good 80% solution, I think. We wouldn't be able to annotate the type of invariant, for example, because it returns void or blows up. I think that's OK.

    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.

    ...
    invariant(isFunction(value));
    value();
    

    flow should be able to type check this correctly as well.

  12. samwgoldman commented on Jul 31, 2015

    @samwgoldman
    Member

    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 isNull as carrying a refinement, but Flow could infer it. Maybe this is obvious behavior, but I don't think anyone has spelled it out yet :)

  13. leebyron commented on Jul 31, 2015

    @leebyron
    Contributor

    That's a great point. I imagine that would work similarly to flow's current return type annotation rules. If your isNull function is inline to the file, hopefully flow would just figure that out for you. If you export function isNull then you should be required to annotate.

  14. 46 remaining items

  15. StreetStrider commented on Jun 4, 2018

    @StreetStrider

    @lukeapage your

      isString(value: string): true;
      isString(value: any): false;

    looks lovely. It's like another way to say isString(value: any): value is String without involving additional syntax, like TS do. That's interesting.

  16. sibelius commented on Jul 12, 2018

    @sibelius

    what is the right way of doing this on Flow?

  17. gajus commented on Jul 14, 2018

    @gajus

    what is the right way of doing this on Flow?

    There is no current way to write % asserts conditions.

    Regarding non-throwing predicate functions, refer to %checks in the https://flow.org/en/docs/types/functions/ documentation.

  18. vicapow commented on Feb 1, 2019

    @vicapow
    Contributor

    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();
      }
    }
  19. bluepnume commented on Feb 1, 2019

    @bluepnume

    Not really fixed, because it's still impossible to write meaningful type-checking functions that don't or can't use typeof or instanceof:

    function isRegex(thing : mixed): boolean %checks {
      return toString.call(thing) === '[object RegExp]';
    }
    
    function match(regex : mixed, str : string) {
      if (isRegex(regex)) {
        regex.test(str);
      }
    }
    
    7:     regex.test(str);
                 ^ Cannot call `regex.test` because property `test` is missing in mixed [1].
    References:
    5: function match(regex : mixed, str : string) {
                              ^ [1]
    

    This encourages instanceof to be overused, which is often the wrong choice.

    Here's a real-world example from React: https://overreacted.io/how-does-react-tell-a-class-from-a-function/

    One caveat to the instanceof solution is that it doesn’t work when there are multiple copies of React on the page, and the component we’re checking inherits from another React copy’s React.Component.

    Ideally flow should have a way to type a isReactComponent function.

  20. StreetStrider commented on Feb 1, 2019

    @StreetStrider

    @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 MyType.is(value: any): boolean which does the check, and not relying on typeof or instanceof.

    Other examples by @bluepnume in topic above.

  21. TrySound commented on Feb 1, 2019

    @TrySound
    Contributor

    Agree, %checks is half working solution.

  22. montogeek commented on Apr 16, 2019

    @montogeek
    Contributor

    It would be great to see this implemented, hopefully we can get it soon.

  23. goodmind commented on Aug 15, 2019

    @goodmind
    Contributor

    This also would allow to remove hardcoded invariant

    declare function invariant(cond: boolean, message: string): empty %asserts(cond)

    /cc @panagosg7

  24. leebyron commented on Sep 16, 2019

    @leebyron
    Contributor

    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 assertIsString() which is really similar to @gabelevi's suggested "explodes" idea in one of the first comments in this issue.

    microsoft/TypeScript#32695

    I'd still love to see both features in Flow!

  25. gajus commented on Nov 28, 2019

    @gajus

    Would be nice to see this prioritised.

  26. SamChou19815 commented on Jul 1, 2023

    @SamChou19815
    Contributor

    We now have type guards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions