Detect common JavaScript errors with TypeScript

TypeScript can help you writing less bugs! Each version improve the detection of common errors. However, all the checks are not enabled by default. In this blog post, we'll see the differents compiler options and the kind of errors they help to catch. There are no specific order beacuse 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 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 the 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. There, it's type is any and you can't check its usage 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 fallthrough cases in switch statement

Sometimes, you can forget the break keywork in a switch case. This may lead to undesired behavior. With the noFallthroughCasesInSwitch compiler option, TypeScript detects the missing break and report an error.

{
    "compilerOptions": {
        "noFallthroughCasesInSwitch": true
    }
}

Disable bivariant parameter checking for function types

The strictFunctionTypes compiler option ensures you are using actually compatible functions (arguments and return type).

{
    "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 actually 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 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 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 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 technics to help you writing even better code, such as the pseudo nameof operator.