Number.isFinite() is not a type guard because it can lead to weird and wrong behaviour in edge cases.
Number.isFinite(4) -> true and the value is a number. That's fine.
Number.isFinite("apple") -> false and the value is not a number. That's fine.
Number.isFinite(Infinity) -> false but the value is not not a number. That's not good.
Some things in JavaScript are of type number but are not finite. These are the values Infinity, -Infinity and NaN. Yes, it stands for "not a number" but it's of type number. It's confusing but makes sense.1
In most cases, we don't care about these non-finite values.
However, as they are still numbers, they are valid in mathematical operations. The concept of infinity is also mathematical and sometimes can be useful.
Discarding these values leads to sometimes wrong code. Let's assume that Number.isFinite() was declared as a type guard. Then consider this code:
function atLeast(minValue: string | number, num: number): boolean {
const min: number = Number.isFinite(minValue)
? minValue
: Number(minValue.replace(/\D/g, "")); //convert
return num >= min;
}
atLeast(2, 42); //true
atLeast("$3.50", 42); //true
atLeast(-Infinity, 42); //error
This would be valid code. Wrong logic and a bit weird but it's here to just illustrate a point. Still, a more realistic usage might be:
let dynamicMinimum = -Infinity; //allow all
/* ... */
someArray.filter(x => atLeast(dynamicMinimum, x));
The code in the function is accepted by TypeScript and when Number.isFinite() returns false type narrowing would cause minValue would not be considered a number and only allowed to be treated as a string by the compiler.
While the example here is contrived and not too hard to figure out it's wrong, you can easily get into a real world situation where you accidentally narrow a value out of being a number when it's still a number. This would be why Number.isFinite() is not a type guard - it can erroneously assert something about a type that is misleading.
1 Here is what I hope is an intuitive explanation why NaN is a number type. In JavaScript values can be changed and some mathematical operations need a number to work with, otherwise they make no sense. But not everything converts to a number. So when you convert a value to a number, you need to somehow signify that it cannot be converted. This is where NaN comes in.
Since the contextual information for what NaN was before conversion, you cannot definitely say it's equal to anything, hence why NaN == NaN is false. An equivalent code might be Number("apple") == Number("orange") - the two values are clearly not equal before the conversion.
First some background. On first glance, this seems to be related to the difference in JavaScript between
isFinite
which will first convert its argument to number and
Number.isFinite
which happily accepts all arguments, regardless of type, and simply returns true only for finite numbers (with no implicit conversions). In TypeScript these have signatures
function isFinite(number: number): boolean
(method) NumberConstructor.isFinite(number: unknown): boolean
In the second method, the parameter type is unknown instead of number. The reason is that in JavaScript, Number.isFinite happy returns false for non-numbers (whereas isFinite coerces first).
> Number.isFinite(false)
false
> isFinite(false)
true
> Number.isFinite(Symbol(2))
false
> isFinite(Symbol(2))
Uncaught TypeError: Cannot convert a Symbol value to a number at isFinite (<anonymous>)
Now since Number.isFinite is already expected to accept all arguments from any type and return false no matter how wacky the arguments, it makes sense for this behavior to be preserved in TypeScript. Hence the parameter type properly is unknown. For the global isFinite which is intended to work only on numbers (and does the coercion that TypeScript helps us avoid), it makes sense to restrict the parameter to being a number.
But as your question rightly asks, why, given that Number.isFinite will accept any arguments and return true only for numbers, why can't it be a type guard? The reason is that it returns true for some, but not all numbers! Let's try at least to write our own guard:
function numberIsFinite(n: any): n is number {
return Number.isFinite(n)
}
and these make sense:
console.log(numberIsFinite(20)) // true
console.log(numberIsFinite(Number.MAX_VALUE)) // true
console.log(numberIsFinite(undefined)) // false
console.log(numberIsFinite("still ok")) // false
But here is where things go wrong-ish:
console.log(numberIsFinite(Infinity)) // false but uh-oh
console.log(numberIsFinite(NaN)) // false but uh-oh
Now it's true that with this type guard that
const x: number|string = 3
if (numberIsFinite(x)) {
console.log(`A number whose type is ${typeof(x)}`)
} else {
console.log(`A string whose type is ${typeof(x)}`)
}
will print
"A number whose type is number"
so far so good but:
const x: number|string = Infinity
if (numberIsFinite(x)) {
console.log(`A number whose type is ${typeof(x)}`)
} else {
console.log(`A string whose type is ${typeof(x)}`)
}
will print
"A string whose type is number"
And this is silly.
If there was a dependent type in TS called "finite number" then (and perhaps only then) would making Number.isFinite a type guard make sense.
Credit to @VLAZ, whose answer this explanation is based on. I added the second part to my original answer after reading theirs.