What you risk when using Number() to parse an integer from a string in TypeScript
Using Number constructor function to parse a number from an input in TypeScript entails a couple of risks. It’s better to be aware of them.
A little while ago, I was working on a change in a typescript-react app with a colleague. We ended up making a breaking change to a major feature, and — as a result — had to update all the usages as well. In one such update, we now had to use a numeric entity id that was present in the url. We used react-router with url params, and react hooks, and the code I wrote went something like this:
And as soon as I wrote this, they had a question for me:
Why not use Number?
Understanding the difference
Let’s dig into the details
Oftentimes in technology — as with a lot of other things in life — there’s no single right way to do things. There are several approaches, each of them with their own pros and cons. Over time, with experience, we form opinions — or our own “smart” defaults. In other words, there’s a first thing we try in every scenario. And the more times it works without problems, the firmer the conviction that it is our best option.
My smart default to get an integer from a string was to use parseInt. The two solutions in consideration by my colleague were:
- Number(id)
- parseInt(id, 10)
Yet, in this case, it’s more than personal preference. I have a couple of points on what we risk by using Number for the specified use case.
Conveying intent
A large part of coding is for the reader
I hate writing comments. Some people love me for it, and others hate me. I prefer to convey intent with code where possible. In that sense, Number and parseInt convey very different intents. They are semantically different.
Number coerces (or converts) any value into a number. parseInt parses an integer value from a string. Given this, parseInt conveys intent better. In other words, it describes what I’m trying to do in a better way. By using Number instead, we risk conveying incorrect intent.
Functional correctness
Do they both actually do the same thing and meet our needs?
The two functions do different things. Number works with any input, but parseInt takes only string inputs. Technically, this is JavaScript with no types, and parseInt coerces any input into a string— but, that is an implementation detail. In non-technical terms, it considers any input you provide to be a string. If we provide a null or undefined input, parseInt will return an NaN (as expected, I hope).
I’ve listed some sample inputs and outputs of both the functions here. Some of the cases that might be baffling are actually reasonable:
Some important distinctions to note here are the different results for:
- null,
- boolean values,
- empty strings,
- strings which represent numbers in scientific notation,
- strings which have explicit octal notation, and
- strings which have non numeric characters.
In this specific instance, the differences in handling a null or empty string as an input matter. Resolving the absence of a parseable input to a valid integer value of 0 is not correct. By using Number, we risk being functionally incorrect.
Type safety
Can we catch issues that otherwise only fail at runtime?
This ties in to both the earlier points, but is unique to TypeScript. The previous two apply to JavaScript and all its variants in general.
Here’s the type definition of the two functions in TypeScript:
Because of this, TypeScript warns us if we call parseInt with an invalid input. If we use Number, we‘re left with no such type safety. This might seem insignificant, but I was able to show its value immediately.
In another usage that my colleague and I had to replace, we had this code:
This used to result in silent runtime errors. TypeScript could’ve caught them at compile time if we used parseInt instead:
If we wrote Number(selected) instead of Number(selected.id), it would go undetected. If we used parseInt instead, we’d avoid this entire class of type-mismatch problems. By using Number, we risk type-safety.
Supplementary info
A few things I would like to clarify
We must treat the NaNs that both of these functions can return.
Wherever I show parseInt in code, it’s always with 2 inputs. The second argument radix is optional in the type definition above, and yet I do this. This is because parseInt has implicit octal and hex detection. The best practice is to always use an explicit radix. In our case, we handle this using a corresponding lint rule so we don’t miss it. It always helps to be explicit, so this is not too unusual.
We have only discussed a certain specific scenario — parsing an integer entity id from a url parameter. In certain other scenarios, Number may indeed be the best option — like dealing with scientific representations of numbers. I hope this article was helpful. The next time you encounter a scenario where you’ve to deal with numbers in strings, I hope you’re able to contemplate the case and make the “right” choice.
Let me know in the comments if you’d like me to address any other specific topics. If you know someone who’d enjoy reading this or find it helpful, please don’t forget to share this with them.