flapenguin.me

Why Number.isNaN is not properly typed in TypeScript

|

Can't it just be typed to return true when the argument is known to be NaN at compile time?

Literal types

In TypeScript you can use literal type to allow only subset of numbers (and not only numbers):

let x: 2 = 2;
x = 2;     // ok
x = -1.23; // not ok: Type '-1.23' is not assignable to type '2'.

You can write a type guard to narrow the type of a variable from number to 2 or any other literal type:

function is_2(x: number): x is 2 { return x === 2; }
const x: number = 2;
if (is_2(x)) { x /* x: 2 */ }

function is_123(x: number): x is -1.23 { return x === -1.23; }
const y: number = -1.23;
if (is_123(y)) { y /* y: -1.23 */ }

So why can't Number.isNaN work the same way?

const x: number = NaN;
if (Number.isNaN(x)) { x /* x: NaN */ }

I tried to type it myself but nothing worked.

// 'NaN' refers to a value, but is being used as a type here.
// Did you mean 'typeof NaN'?
function is_NaN(x: number): x is NaN { return Number.isNaN(x); }

As you say, compiler.

// type type_NaN = number
type type_NaN = typeof NaN;

Maybe I can overcome this by forcing it to const?

// A 'const' assertions can only be applied to references to enum members,
// or string, number, boolean, array, or object literals.
const myNaN = NaN as const;
function is_NaN(x: number): x is typeof myNaN { return Number.isNaN(x); }

All this happens because if you try to look at the type of NaN you'll get this

// var NaN: number
NaN

At first this seems a very strange behavior. From a programmer's point of view NaN while being Not a Number is still a valid number, not much different from -1.23 from the example. And it is so both in TypeScript and in JavaScript.

The problem

The problem is that NaN is not a keyword or a number literal in terms of syntax. But rather a readonly non-configurable property on the global object (spec), like others properties on window like setTimeout. This is so both in TypeScript and in JavaScript.

This means you can use NaN as a regular identifier: as an argument name, as a variable name, and so forth.

const NaN = 'top';
console.log(NaN); // top

((NaN) => console.log(NaN))('kek'); // kek

The story is the same with Infinity and undefined. (The latter should not be a surprise to an old-timer)

The (nonexistent) solution

At first glance there is an easy solution. Just make NaN (and undefined with Infinity for the company) a keyword or a number literal constant in TypeScript, for easier typing.

This will lead to a serious consequences. TypeScript will no longer be able to parse and typecheck some valid JavaScript. Which means it will not be a superset of JavaScript anymore.

We can't "fix" TypeScript here without fixing JavaScript. So let's fix JavaScript! Let's make NaN a keyword in EcmaScript 2023!

But wait, we can't change JavaScript spec without breaking backwards compatibility of the web and unknown number of written code.

You can argue that no one in their right mind would name a variable or an argument NaN. And if they had then they should suffer. Then imagine your online banking web app not working because some 20+ years old code is suddenly broken in new Chrome/Firefox/Safari.

Backward compatiblity is hard but crucial for web.

Or in other words: we're stuck with this.

(There's multiple reasons why such typing is problematic anyway because of how IEEE-754 works. But let's not dig into that rabbit hole for now.)