Detect common JavaScript errors with TypeScript
TypeScript can help you with writing less bugs! Each version improves the detection of common errors. However, all the checks are not enabled by default. In this blog post, we'll see the different compiler options and the kind of errors they help to catch. There is no specific order because all of them are important.
If you're still not using TypeScript, you can read my previous post: Still not using TypeScript?.
#Invalid arguments, unknown method/property, or typo
TypeScript knows what is valid or not as it knows the types of the variables, or functions. This prevents typo or using an unknown method or property, or using an argument of type string instead of a number, etc. So, you don't need to execute your code to detect this kind of issue.
var author = { nickname: "meziantou", firstname: "Gérald", lastname: "Barré" };
console.log(author.lastName); // Error: property 'lastName' does not exist on type '...'. Did you mean 'lastname'?
author.nickname.trimStart(); // Error: property 'trimStart' doest not exist on type 'string'.
author.firstname.charCodeAt("1"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
#Null- and undefined-aware types
I think strictNullChecks
is the most important TypeScript compiler flag. This will help you detect lots of potential errors. We often suppose a value is not null
or undefined
, which is not always true. The strictNullChecks
option considers null
and undefined
as different types. So, if a variable can be null, you must explicitly declare it as type | null
;
{
"compilerOptions": {
"strictNullChecks": true
}
}
function a(s: string) {
console.log(s.length); // ok
}
a(null); // error: null is not assignable to string
function b(s: string | null) {
console.log(s.length); // error: s is probably 'null'
}
function c(s?: string) {
console.log(s.length); // error: s is probably 'undefined'
}
#Report error when not all code paths in function return a value
The noImplicitReturns
compiler option ensures you always write a return statement in all branches of a method if at least one branch contains a return statement with a value.
{
"compilerOptions": {
"noImplicitReturns": true
}
}
// Error: Not all code paths return a value.
function noImplicitReturns(a: number) {
if (a > 10) {
return a;
}
// No return in this branch
}
#Report error on unreachable code.
The allowUnreachableCode
compiler option ensures you don't have dead code in a function. It detects unreachable code such as if (constant)
or code after returns. Note that in JavaScript, the return
statement ends at the end of the line. So, if you write the value of the return statement on the next line, the function will return undefined
, and the next line is never executed.
{
"compilerOptions": {
"allowUnreachableCode": false
}
}
function allowUnreachableCode() {
if (false) {
console.log("Unreachable code");
}
var a = 1;
if (a > 0) {
return
10; // Unreachable code
}
return 0;
console.log("Unreachable code");
}
#Report errors on unused locals or unused parameters
The noUnusedLocals
and noUnusedParameters
compiler options ensure you don't have unused variables or parameters. This may occur after code refactoring, and just because you forget a part of your algorithm. For the compiler, an unused parameter or variable is a parameter or a variable with no read access.
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
function f() {
var a = "foo"; // Error: 'a' is declared but its value is never read
return "bar";
}
function f(n: number) {
n = 0; // Never read
}
class C {
private m: number; // this.m is never read
constructor() {
this.m = 0;
}
}
#Report errors on excess property for object literals
The suppressExcessPropertyErrors
compiler option check your don't have unexpected properties in an object. This may help you detecting typo in your code.
{
"compilerOptions": {
"suppressExcessPropertyErrors": false
}
}
var x: { foo: number };
x = { foo: 1, baz: 2 }; // Error, excess property 'baz'
var y: { foo: number, bar?: number };
y = { foo: 1, baz: 2 }; // Error, excess property 'baz'
#Report errors on this
expressions with an implied any
type
A function's this keyword behaves a little differently in JavaScript compared to other languages. In most cases, the value of
this
is determined by how a function is called. It can't be set by assignment during execution, and it may be different each time the function is called.
You can read more about this in the MDN documentation.
TypeScript can't detect automatically the type of this
in a function because the value of this
depends on the way the function is called. In this case, TypeScript uses the any
type and you can't check usages correctly. You can prevent this behavior by forcing the typing of this
. Thus, your code will be typed, and you'll get all the benefices of that.
{
"compilerOptions": {
"noImplicitThis": true
}
}
function noImplicitThis() {
return this.length; // Error: 'this' implicitly has type 'any' because it does not have a type annotation
}
You can correct your code by typing this
:
function noImplicitThis(this: string[]) {
return this.length; // ok
}
#Report errors for fall through cases in switch statement
Sometimes, you can forget the break
keyword in a switch case
. This may lead to undesired behavior. With the noFallthroughCasesInSwitch
compiler option, TypeScript detects the missing break
and reports an error.
{
"compilerOptions": {
"noFallthroughCasesInSwitch": true
}
}
#Disable bivariant parameter checking for function types
The strictFunctionTypes
compiler option ensures you are using compatible functions (arguments and return types).
{
"compilerOptions": {
"strictFunctionTypes": true
}
}
interface Animal { name: string; };
interface Dog extends Animal { breed: string; };
var f = (animal: Animal) => animal.name;
var g = (dog: Dog) => dog.breed;
f = g; // error: g is not compatible with f
// error: Type '(dog: Dog) => any' is not assignable to type '(animal: Animal) => any'.
// Types of parameters 'dog' and 'animal' are incompatible.
// Type 'Animal' is not assignable to type 'Dog'.
// Property 'dogProp' is missing in type 'Animal'.
The following blog post explains in details why those 2 functions are incompatible: https://blogs.msdn.microsoft.com/typescript/2017/10/31/announcing-typescript-2-6/
- Is it okay for a value of type (dog: Dog) => any to say it can be used in place of a (animal: Animal) => any?
- Is it okay to say my function only expects an Animal when it may use properties that on Dog?
- Only if an Animal can be used in place of a Dog – so is Animal assignable to Dog?
- No! It's missing dogProp.
#Report errors for indexing objects lacking index signatures
The suppressImplicitAnyIndexErrors
option prevents you from accessing properties using the indexer syntax unless the property is defined or an indexer is defined.
{
"compilerOptions": {
"suppressImplicitAnyIndexErrors": false
}
}
var x = { a: 0 };
x["a"] = 1; // ok
x["b"] = 1; // Error, type '{ a: number; }' has no index signature.
#Parse in strict mode and emit "use strict" for each source file
The alwaysStrict
compiler option indicates the compiler to always parse the file in strict mode and to generate "use strict";
, so you don't have to set it in every file. If you don't know what is the strict mode, I should read the great post from John Resig: https://johnresig.com/blog/ecmascript-5-strict-mode-json-and-more/
Strict mode helps out in a couple of ways:
- It catches some common coding bloopers, throwing exceptions.
- It prevents, or throws errors when relatively "unsafe" actions are taken (such as gaining access to the global object).
- It disables features that are confusing or poorly thought out.
{
"compilerOptions": {
"alwaysStrict": true
}
}
#Report errors on unused labels
The usage of labels is very uncommon. If you don't know them, it's ok. If you are curious, you can jump to the documentation. TypeScript provides a compiler option to ensure you don't have an unused label in your code.
{
"compilerOptions": {
"allowUnusedLabels": false
}
}
loop1:
for (let i = 0; i < 3; i++) {
loop2: // Error: Unused label.
for (let j = 0; j < 3; j++) {
break loop1;
}
}
#Report errors on expressions and declarations with an implied any
type
When the TypeScript compiler cannot determine the type of an object, it uses any
. Therefore, you cannot get the advantages of the type checker. This is not what you want when you use TypeScript, so the compiler raises an error. You can fix this error by specifying the type.
{
"compilerOptions": {
"noImplicitAny": true
}
}
function noImplicitAny(args) { // Error: Parameter 'args' implicitly has an 'any' type.
console.log(args);
}
function noImplicitAny(args: string[]) { // ok with the type information
console.log(args);
}
#Report errors in *.js files
If you use js files along with ts files, you can check them using TypeScript. The compiler will use information from JSDoc, imports, and usages to validate your code. The verification won't be as powerful as for ts files, but this is a good start. You can read more about how the compiler handle js files in the wiki: https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files
{
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
}
#Conclusion
TypeScript has a lot of options to increase the robustness of your code. By enabling all of them, you'll reduce the number of errors at runtime. Then, you can use some nice techniques to help you write even better code, such as the pseudo nameof operator.
Do you have a question or a suggestion about this post? Contact me!