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!