6

The built-in type number in Flow allows the "exotic" values such as Infinity, -Infinity and NaN.

How can I enforce the type to only allow real numbers?

EDIT. This is not a question how to check if a variable is real number. It is about typing with Flow.

I am looking for the way to write my functions like:

// @flow
function sum (x: real, y: real) { ... }

My question is how to define the type real so it works with Flow (http://flowtype.org/).

Dmitri Zaitsev
  • 13,548
  • 11
  • 76
  • 110

2 Answers2

4

You can't do that using Flow. You'll need runtime checks.

See discussion of the issues with real numbers here: https://github.com/facebook/flow/issues/1406

The bottom line is, pretty much any operation on real numbers could result in infinity, so distinguishing between real numbers and NaN / Infinity would not be very useful as it would return a type than is not guaranteed to be real.

For example,

Number.MAX_VALUE + Number.MAX_VALUE === Infinity
-Number.MAX_VALUE - Number.MAX_VALUE === -Infinity
Number.MAX_VALUE * 2 === Infinity
Number.MAX_VALUE / 0.5 === Infinity

Separately from that discussion, Flow doesn't have any facilities to blacklist certain values while allowing other values of the same type. You can only whitelist values, including using unions and intersections.

Nikita
  • 2,924
  • 1
  • 19
  • 25
  • 1
    Thanks, see the proposal updated: https://github.com/facebook/flow/issues/1406#issuecomment-219905558 – Dmitri Zaitsev May 18 '16 at 02:05
  • Basically a whitelisting is suggested. None of your examples would be whitelisted (see the issue). – Dmitri Zaitsev May 18 '16 at 02:15
  • In the context of your github comment: if I have numbers `a: nonzero` and `b: nonzero`, then for example `a + b` would be of type `finite`, not `nonzero` because Flow's type system could never prove otherwise. So in practice you'd need to check for 0 at runtime to refine the result of such operations to `nonzero`, similarly to how we check for `null` to refine nullable types into non-nullable types. Note that this will work just as well without a special `nonzero` type. – Nikita May 18 '16 at 04:31
  • 1
    Yes, `a + b` becomes `finite`, so it is not safe anymore inside division expression. So it has be guarded for with addition code that Flow can pick to make sure it is safe. This can be all done at compile-time. Without a `nonzero` type I don't see how to do it. – Dmitri Zaitsev May 18 '16 at 04:41
3

There is a bit of middle ground you can strike with this with Flow. Yes, you'll need to use a runtime check to pull this off in the end, but you can construct an opaque type that will let Flow enforce you cannot bypass those validation functions. First, in one file, put this:

// @flow

// Define `Int` as an opaque type.  Internally, it's just a number.
// It's opaque because only this module can produce values of
// this kind, so in order to obtain an "Int", one _must_ use one of
// these functions, which (at runtime) will guarantee that these
// will always be integers.
export opaque type Int: number = number;

// Here's a function that will convert any number to an Int by running
// a typecheck at runtime and perhaps change the value (by rounding)
// This is the ONLY way of obtaining a value of the type Int   
export function int(n: number): Int {
    if (!Number.isFinite(n)) {
        throw new Error('Not a (finite) number');
    }

    // Round any real numbers to their nearest int
    return Math.round(n);
}

// In your private functions, you can now require Int inputs
export function isPrime(n: Int): boolean {
    // In here, you can assume the value of `n` is guaranteed to be an Integer number
    for (let i = 2; i < Math.sqrt(n); i++) {
        if (n % i === 0) return false;
    }
    return true;
}

Then, you use those like so:

// @flow

import { int, isPrime } from './lib';

isPrime(int(NaN));   // ok, but a runtime error, because NaN is not a number!
isPrime(int(3.14));  // ok, will effectively become isPrime(3)
isPrime(3.14);       // Flow error!
nvie
  • 366
  • 2
  • 10
  • This is clever, thanks. I'm not sure it still works though - I'm seeing Flow complain it can't do arithmetic with Int. – Peter W Jun 24 '20 at 01:08
  • 1
    In order to be able to do that, you'll have to declare that `Int` is a "subclass" of `number`. You do that by changing the definition of the opaque type like so: `export opaque type Int: number = number;` (I've updated the original answer.) This will allow you to do math like normal: `const area = (n: Int): number => n * n * Math.PI;`. Do note that the result of any "normal" math operation will be a `number` type again (it won't retain `Int` here). – nvie Jun 24 '20 at 07:14