27 Native JavaScript Features for Type-Checking & Code Integrity
JavaScript has many built-in features that increase the strictness and correctness of your code, including type-checking. Getting familiar with and using these features are critical to writing safe and robust code that's not going to fail at runtime.
Here they all are (I think I got them all), each with a link to MDN and a brief explanation of their use case:
Type Checking Features
These are useful for checking the type of an object or comparing the types of objects.
The typeof operator returns a string indicating the type of the unevaluated operand.
It's not perfect, but it enables type-checking for string
, number
, bigint
, boolean
, function
, symbol
, object
, and undefined
.
Object.prototype.toString.call(obj)
Every object has a
toString()
method...toString()
returns "[object type]", wheretype
is the object type.
This method can check object types like Array
, Date
, RegEx
, and more. This is best wrapped up in a little helper function.
The
instanceof
operator tests to see if theprototype
property of a constructor appears anywhere in the prototype chain of an object.
Great for checking types like Array
, Date
, RegEx
, and custom types. There's also a more verbose, but self-explanatory way of checking: Object.getPrototypeOf(obj) === MyClass.prototype
. Arrays have a gotcha, see next.
The
Array.isArray()
method determines whether the passed value is anArray
.
There are edge cases that make using this method safer than instanceof
.
The
Number.isInteger()
method determines whether the passed value is an integer.
There are edge cases to be aware of as well as Number.isSafeInteger()
. Also, don't forget decimals will evaluate to false
.
Number.isFinite()
and isFinite()
Static method determines whether the passed value is a finite number — that is, it checks that a given value is a number, and the number is neither positive Infinity, negative Infinity, nor NaN.
Same as isInteger()
but these include decimals as well. The global isFinite()
will convert the value to a number before checking.
Determines whether the passed value is the number value
NaN
, and returnsfalse
if the input is not of the Number type.
A reliable way to check for the NaN
value. Note that using strict equality (===) does not work as expected on NaN values. The global isNaN()
will convert the value to number before checking.
The strict equality operator checks whether its two operands are equal...[it] always considers operands of different types to be different.
Skips type coercion for a more accurate comparison.
Object Integrity Features
These are useful for ensuring that code is accessing is what you expect it to be.
The value of a constant can't be changed through reassignment, and it can't be redeclared.
Variables declared with var
and let
can potentially be reassigned a value your code won't handle correctly, so using const
helps protect against this.
The optional chaining operator permits reading the value of a property located deep within a chain of connected objects without having to expressly validate that each reference in the chain is valid.
Optional chaining is one of the most impactful additions to web development in recent years. It's our most powerful tool to fight Uncaught TypeError: Cannot read property
, which has been identified by Rollbar as the number one production JavaScript error (see Top 10 JavaScript Errors From 1,000 Projects).
Data shapes in web apps can be unpredictable because most data originates somewhere outside the app's code, like your services, 3rd-party services, hosted files and objects, user input, storage, and more. Even well-defined custom types can fail to account for everything (I've seen multiple projects where the app's TypeScript definitions were not kept up-to-date with the API's data models), so even with TypeScript, you should use optional chaining on all data your code doesn't originate.
hasOwnProperty()
and in
The
hasOwnProperty()
method returns a boolean indicating whether the object has the specified property as its own property (as opposed to inheriting it).
When you need to verify that a property exists directly on an object, use this. Use in
only when you know that checking the object and its prototype chain is acceptable.
The nullish coalescing operator is a logical operator that returns its right-hand side operand when its left-hand side operand is
null
orundefined
, and otherwise returns its left-hand side operand.
This is needed when you can't tolerate falsy rules, i.e. you need to accept 0
, ''
, or false
values.
Pair it with an assignment - const bar = foo ??= 'bar'
- to ensure nullish values are replaced with something valid.
The
Object.is()
method determines whether two values are the same value.
Its rules for equality are slightly different than ===
and ==
.
The
Object.seal()
method seals an object, preventing new properties from being added to it...Values of present properties can still be changed as long as they are writable.
This is like const
on steroids. The shape of the object cannot change - you can't add or remove properties - but you can edit their values.
The
Object.freeze()
method freezes an object. A frozen object can no longer be changed [in any way].
Like seal()
, but you can't even edit existing properties. Frozen means nothing about that object can be changed, but one thing to remember is an object's "values that are objects can still be modified, unless they are also frozen."
Type Conversion Features
There are often times where you don't only want to check if a value is a certain type, but to convert it if it's not. There are many features we can use for this.
The equality (==) operator checks whether its two operands are equal...it attempts to convert and compare operands that are of different types.
Useful when you are comparing to null
When called as a function, it returns primitive values of type Boolean.
Reliably convert a value to true
or false
. It follows truthy/falsy rules.
When called as a function, it returns primitive values of type Number.
Use when you want the value converted to a number. For example, Number('')
returns 0
but Number('100px')
returns NaN
.
parses a string argument and returns an integer of the specified radix
Use when you want the value parsed and into a number. For example, parseInt('')
returns NaN
but parseInt('100px')
returns 100
. Use parseFloat()
for decimals.
Error Handling
After all has been done to prevent errors, there are still cases where you need to handle potential errors. For example, virtually every fetch
should have an error handler.
Promise.catch()
and Promise.then(successCallback, errorCallback)
schedules a function to be called when the promise is rejected.
If nothing else, add this to every fetch: fetch(url, init).catch(console.error)
.
The code in the
try
block is executed first, and if it throws an exception, the code in thecatch
block will be executed.
Catching errors can enable your app to continue to work despite the issue. There are 8 error types in JavaScript.
What about TypeScript?
It's important to remember that TypeScript does not guarantee correctness at runtime. It only does static type checking, which means it scans the text of your source code for errors. That's not nearly enough. If you've never worked in a typed language like Java or C#, then you're probably not familiar with the "But it compiled" sentiment of those developers. They are painfully aware of how a successful compilation isn't a guarantee their software works and it is even more true with TypeScript. So, instead of pouring on layer after layer of TypeScript syntax and "just shut up and build!" hackery, simply write safe and robust vanilla JavaScript using the features above. If you want the IDE experience that TypeScript can give you, those same benefits are easily had with JSDoc typedefs (and other annotations too). JSDoc requires no setup, no proprietary code or tooling, and doesn't increase your build time or complicate your project's configuration.