Typescript Best Practices

Learning Typescript as a backender

photo of Thi Hong Van Phan
Thi Hong Van Phan

Software Engineer

Posted on Feb 14, 2019

Typescript is becoming more and more popular. As with everything, there are good and bad sides. How good it is depends on your usage on your application. This article will not discuss the good and bad sides of Typescript but some best practices, which will help for some cases to get the best out of Typescript.

1. Strict configuration

Strict configuration should be mandatory and enabled by default, as there is not much value using Typescript without these settings. Without it, programs are slightly easier to write but you also lose many benefits of static type checking. The flags that need to be enabled in tsconfig.json are:

    {
        "forceConsitentCasingInFileNames": true,
        "noImplicitReturns": true,
        "strict": true,
        "noUnusedLocals": true
    }

The most important one is the "strict" flag, which covers four other flags that you can add independently:

  • noImplicitThis: Complains if the type of this isn’t clear.

  • noImplicitAny: With this setting, you have to define every single type in your application. This mainly applies to parameters of functions and methods.

    const fn = ( worker ) => worker.name

If you don’t turn on noImplicit, any worker will implicitly be of any type.

  • strictNullChecks: null is not part of any type (other than its own type, null) and must be explicitly mentioned if it is an acceptable value.
    interface Worker {
       name: string;
    }
    const getName = (worker?: Worker) => worker.name

This code snippet won’t compile because "worker" is an optional parameter and can be undefined.

  • alwaysStrict: Use JavaScript’s strict mode whenever possible.

For further compiler options please find them here:

https://www.typescriptlang.org/docs/handbook/compiler-options.html

2. General types - prefer to use primitive types

Use the primitive type number, string, boolean instead of String, Boolean, Number. These types refer to non-primitive boxed objects which are never appropriately used in Javascript.

3. Type inference

Instead of explicitly declaring the type, let the compiler automatically infer the type for you. Because it knows better which type it is:

    let name = 'David';  //name is string
    let age = 11; // age is number

4. Callback types

By callback which returns value, can be ignored. Other case using void instead of any is prefered:

    function cal(x: () => any) {
        var y = x();
        y.doAnything();  // ok but unchecked
    }

Using void is safer because it prevents using any value, which could be unchecked:

    function cal(x: () => void) {
        var y = x();
        y.doAnything(); // Error
    }

5. Function parameters

By function with a lot of parameters or parameters with the same type. It makes sense to change the function to take parameter as an object instead

    function cal(x: string, y: string, z: string) {}

By such a function, it’s quite easy to call it with the wrong order of parameters. For instance: cal(x, z, y) Change the function to take an object:

    function cal(foo: {x: string, y: string, z: string}) {}

The function call will look like: cal({x, y, z}) which makes it easier to spot mistakes and review code.

6. Overloads - Ordering

The more specific overloads should be put after the more general overloads. Example:

     interface Person {}
     interface Worker extends Person {}
     function tun (w: Worker) : number;
     function tun (p: Person) : string;
     function tun (a: any) : any;
     var w: Worker;
     var y = tun (w); // y: any

Should define the following order:

     declare function tun (a: any) : any;
     declare function tun (p: Person) : string;
     declare function tun (w: Worker) : number;
     var w: Worker;
     var y = tun (w); // y: number

Because the first matching overload will be resolved. When the more general one is declared, the less general one will be hidden.

Overload - use optional parameter

In the following example, you can use optional parameter(s) for only one declared function

    interface Business {
       cal (x: string) : number;
       cal (x: string, y: string) : number;
       cal (x: string, y: string, z: number) : number;
    }
    interface Business {
       cal (x: string, y?: string, z?: number) : number;
    }

But it only works for functions which have the same return type.

Overload - use union type

    interface Business {
       cal () : string;
       cal (x: string) : number;
       cal (x: number) : number;
    }

Instead you might use union type like this:

    interface Business {
       cal () : string;
       cal (x: string | number) : number;
    }

7. Don’t use "bind"

"bind" returns any. If you take a look into the definition of bind:

    bind (thisArg: any, ...anyArray: any[]) : any

This means that by using bind it’ll always return "any" and for bind() there is no type check, it accepts any type:

    function add (x: number, y: number) {
       return x + y;
    }
    let curryAdd = add.bind(null, 111);
    curryAdd(333); // Ok but no type checked
    curryAdd('333') // Allowed because no type check

Better to write it with arrow function:

     let curryAdd = (x: number) => add(111, x);
     curryAdd(333) // Ok and type check
     curryAdd('333') // Error

So that with the static type check, the compiler discovers the wrong type and does not silently allow for any type by binding. But in the new version of Typescript there will be more strictly-typed for "bind" on function types.

8. Non existing value - prefer to use undefined as null

When a value on an object property or a function parameter is missing, you can use Typescript optional "?" to say so.

     interface Worker {
        name: string;
        address?: string;
     }

Typescript "?" operator doesn’t handle null value. There are two values: null and undefined, but actually null is a representation of a missing property. It’s then the same as undefined. That’s why it’s recommended to use undefined for non existing values and forbid the use of null using TSLint rule:

{ "no-null-keyword": true }

It’s impossible to use typescript optional "?" to define a variable or function return type as undefined. In order to try to safely handle a missing 'worker', before using its property, typescript can actually infer the type of a parameter with type guards and we can actually use this to unwrap our optional worker:

    type Optional = T | undefined
    const getName = (worker?: Worker) => {
        if(worker) {
           return worker.name;
        }
        return 'no worker';
    }
    let worker: Optional;

or

    let worker: Worker | undefined;
    console.log(getName(worker));   // 'no worker'

The above code snippet will print 'no worker' because our worker is not defined but with this abstraction type we’ve safely handled a missing object use case. So the "Optional" would be a little bit shorter and have the same result.



Related posts