Categories
TypeScript Best Practices

TypeScript Best Practices —void, Default type Parameters, Spread, and Numbers

No void Expressions

We shouldn’t return expression in function with the void return type.

For instance, if we have the doWork function:

const doWork = (): void => {
  doFirst();
  doSecond();
};

We should call doWork without using its return value.

So instead of writing:

console.log(doWork());

We write:

doWork()

No Conditional Expression

We should use conditional expressions instead of assigning the same thing to each branch of the statement.

For instance, instead of writing:

let foo;
if (cond) [
  foo = 1;
}
else {
  foo = 2;
}

We write:

let foo = cond ? 1: 2;

It’s much shorter and easy to read.

Use Object Spread

Object spread has been a feature since ES2018, so we should use it instead of Object.assign .

For instance, instead of writing:

const baz = Object.assign({}, foo, bar);

We write:

const baz = {...foo, ...bar};

Including the Radix Argument in parseInt

We should include the radix argument when calling parseInt so that it’ll parse the value to the number with the base we want.

It also won’t assume the base of the number we’re parsing based on the value.

For instance, instead of writing:

const x: string = '12';
const dec: number = parseInt(x);

We write:

const x: string = '12';
const dec: number = parseInt(x, 10);

Operands Should be of Type String or Number When Using the Plus Operator

We should make sure that they’re both numbers of strings when we’re using the + operator.

This way, we know that we’re adding or concatenating for sure.

For instance, instead of writing:

const foo = 'foo' + 1;

We write:

const foo = 'foo' + 'bar';

or:

const sum = 1 + 2;

No Usage of this in Static Methods

We shouldn’t use this in static methods.

They shouldn’t reference this since they won’t reference values we expect, which is the class instance.

For instance, instead of writing;

class Foo {
  static foo() {
    return 'foo';
  }

  static bar() {
    return `bar${this.foo()}`;
  }
}

We write:

class Foo {
  foo() {
    return 'foo';
  }

  bar() {
    return `bar${this.foo()}`;
  }
}

Strict Comparisons

We should make sure that we use === and !== for equality comparisons.

They check the type of data and the value.

So instead of writing:

x == 1;

We write:

x === 1;

to avoid any data type coercion before comparison.

Strict String Expressions

We should use interpolation with template literals instead of using string concatenation.

For instance, instead of writing:

'foo' + bar

We write:

`foo ${bar}`

Add Default Clause with Switch Statements

We should add a default clause with switch statements so that we do something when none of the case s match the values.

For instance instead of writing:

let foo: number = 1;
switch (foo) {
  case 1:
    doSomething();
    break;
  case 2:
    doMore();
}

We write:

let foo: number = 1;
switch (foo) {
  case 1:
    doSomething();
    break;
  case 2:
    doMore();
    break;
  default:
    console.log('default');
}

Compare typeof with the Correct String Values

We should compare typeof with the correct string values.

typeof can only return a few string values for the types.

So we should compare against them and make sure we don’t have any typos.

For instance, instead of writing:

typeof foo === 'undefimed';

We write:

typeof foo === 'undefined';

No Unnecessary Constructor

We shoudn’t have constructors that are redundant.

JavaScript will add them for us without it.

For instance, instead of writing:

class A {
  constructor(){}
}

or

class A extends B {
  constructor(){
    super();
  }
}

We write:

class A {
  constructor(name){
    this.name = name;
  }
}

or

class A extends B {
  constructor(bar){
    super(bar);
  }
}

Default Type Parameter

We can add a default type value to the generic type parameter.

For instance, instead of writing:

function foo<N, S>() {}

We can write:

function foo<N = number, S = string>() {}

This way, the generic type parameters will always be set with an argument.

Conclusion

We can add default types to generic type parameters.

void expressions shouldn’t be used as values.

Conditional expressions, object spread, parseInt with a radix are all things we should have in our code.

Categories
TypeScript Best Practices

TypeScript Best Practices — Switches, Expressions, and Downcasting

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

No String Literal Property Access

We shouldn’t use string literals to access properties if they’re valid identifiers.

For instance, instead of writing:

obj['prop']

We write:

obj.prop

But we can still use brackets to access properties that aren’t valid identifiers.

For instance, we can still write:

obj['foo-bar'];

No Throwing Strings

We shouldn’t throw strings since we toss out lots of information like the stack trace and the error type.

For instance, instead of writing:

throw 'error';

We write:

throw new Error("error");

No Switch Case Fallthrough

We shouldn’t have switch cases that don’t have a break or return statement.

For example, instead of writing:

switch (foo) {
  case 1:
    doWork(foo);
  case 2:
    doOtherWork(foo);
}

We write:

switch (foo) {
  case 1:
    doWork(foo);
    break;
  case 2:
    doOtherWork(foo);
    break;
}

No Tautology Expressions

We shouldn’t compare a value with itself.

They always run or don’t run.

For instance, instead of writing:

3 === 3

We write:

val === 3

No Assignment to this

We shouldn’t assign this to a value.

If we need to do that, we should use arrow functions instead.

So instead of writing:

const self = this;

setTimeout(function() {
  self.work();
});

We write:

setTimeout(() => {
  this.work();
});

No Unbound Method

We shouldn’t use instance methods outside a method call.

For instance, instead of writing:

class Foo {
  public log(): void {
    console.log(this);
  }
}

Foo.log();

We write:

class Foo {
  public log(): void {
    console.log(this);
  }
}

const foo = new Foo();
foo.log();

We can also write an arrow function and use bind if we don’t need to reference the class instance:

class Foo {
  public log = (): void => {
    console.log(foo);
  }
}

`const foo = new` Foo`();
foo.`log()`;`

or:

class Foo {
  public log() {
    console.log(this);
  }
}

const foo = new Foo();
const manualLog = foo.log.bind(foo);
manualLog.log();

No Unnecessary Class

We can put lots of things outside of classes.

For instance, we can have top-level functions and variables that aren’t contained in a class.

No Unsafe any Casts

We shouldn’t cast any variable or value to any .

This means it can contain anything.

If it has dynamic properties, we can use index signatures, union or intersection types, and more.

Instead of casting to any :

const foo = bar as any;

We can write:

interface Bar {
  [key: string]: string | number;
  [index: number]: string;
  baz: number;
}

const foo: Bar = bar;

We have baz and other dynamic properties in the Bar interface.

No Unsafe finally

We shouldn’t have return , continue , break or throws in finally blocks since they override the control flow statements of the try and catch blocks.

For instance, instead of writing:

try {
  doWork();
}
catch(ex) {
  console.log(ex);
}
finally {
  return false;
}

We write:

try {
  doWork();
  reutrn true;
}
catch(ex) {
  console.log(ex);
  return false;
}

No Unused Expression

We shouldn’t have any unused expression statements in our code.

For instance, instead of writing:

a === 1

We remove them or use them in statements.

No Using Variables Before Declaring Them

We shouldn’t use variables declaring them.

This is possible with var , but throws errors with let and const .

Instead of writing:

console.log(x);
var x = 1;

We write:

var x = 1;
console.log(x);

let y = 2;
console.log(y);

const z = 3;
console.log(z);

No var Keyword

We should think twice about using the var keyword.

Instead of using var , which has a tricky function scope and hoisting, we use let and const which are block-scoped and no tricky scoping issues.

For example, instead of writing:

var x = 2;

We write:

let a = 1;
const bar = 3;

Conclusion

We should access properties with the dot notation whenever possible.

Also, we should use the modern syntax for declaring variables.

Downcasting to any is also not good since we can assign anything to it.

We should also have break or return in our case blocks.

Categories
TypeScript Best Practices

TypeScript Best Practices — Literal Types and Promises

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

No delete Expressions with Computed Key Expressions

Using delete to remove computed key expressions is a bad idea.

It’s not secure and it can’t be optimized.

So instead of writing:

delete foo[bar];

We can just leave the object as is.

No Empty Blocks

We shouldn’t have empty blocks in our code.

They’re useless.

For instance, we should remove code like:

function foo(){}

No Floating Promises

Floating promises re promises that aren’t storing or returning any data.

Therefore, it’s not actually doing anything.

For instance, if we have:

Promise.resolve(1);

Then we should do something with it.

Unhandled promises can lead to unexpected behavior since they give us a value in an indeterminate amount of time.

Don’t Use for-in Loops with Arrays

We shouldn’t use for-in loops with arrays. It’s meant to be used with regular objects.

The order of iteration isn’t guaranteed.

So instead of using for-in like:

for (const i in array){
  console.log(array[i]);
}

We can write:

for (const a of array){
  console.log(a);
}

We replaced in with of and a for the array item.

Now we can loop through them easily in order.

No Inferred Empty Object Type

We shouldn’t have empty object types since we should have a more specific type for our variables, parameters, and return types.

Instead of writing:

const obj: {} = {};

We write:

const obj: { foo: number } = { foo: 2 };

No Invalid this

We shouldn’t use this outside of classes or object literals.

It just causes confusion since this is supposed to be a class instance.

For instance, we can write:

const obj = {
  a: 1,
  b() {
    console.log(this.a)
  }
}

or:

class Foo {
  constructor() {
    this.a = 1;
  }

  bar() {
    console.log(this.a)
  }
}

No null Keyword

We should just use undefined for all empty values instead of mixing null or undefined .

They serve the same purpose so we don’t need both.

undefined also has type undefined so it’s easier to check it.

Therefore, just use undefined everywhere in our code.

No null and undefined Union Types

Creating a type with null and undefined is redundant.

This is because our variable, parameter or returned value can just have null or undefined or any other we specified.

We should make the type more restrictive so that we can actually take advantage of the type checking capability of TypeScript.

No Object Literal Type Assertion

Having type assertion with object literal types will hide errors with excess properties.

Even if the object is missing some required fields, the data type assertion will override whatever type it has without the assertion.

Therefore, we should deal with it without forcing the type to be an object literal structure.

So we shouldn’t write:

const foo = {} as { bar: number };

We write:

const foo: Foo = {};

where Foo is an interface, type or type alias.

No Promise as Boolean

We shouldn’t use an await expressions as a boolean expression.

await only has a promise for giving the actual value in the future.

It’s not the actual value we want to check for.

Therefore, we should assign the await ed value into a new variable so that we can do the check with the actual value.

For instance, instead of writing:

async function bar(personPromise: Promise<Person> ) {
  if (await personPromise) {
    console.log("person retrieved")
  }
}

We write:

async function bar(personPromise: Promise<Person> ) {
  const person = await personPromise;
  if (person) {
    console.log("person retrieved");
  }
}

No Return Await

We shouldn’t use return and await on the same line.

It just adds extra time before the promise is resolved but the promise is returned no matter if there’s an await or not.

For instance, instead of writing:

async function bar() {
  return await aPromise;
}

We write:

async function bar() {
  return aPromise;
}

Conclusion

delete operator shouldn’t be used with the computed expression.

for-in loops shouldn’t be used with arrays.

this should only be used for object literals and classes.

Literal type assertions shouldn’t be used to force the structure of the object.

await should be used in certain places only in async functions.

Categories
TypeScript Best Practices

TypeScript Best Practices — Non-Null Assertions, Async, and Loops

To make code easy to read and maintain, we should follow some best practices.

In this article, we’ll look at some best practices we should follow to make everyone’s lives easier.

Non-Null Assertion

We can add a non-null assertion operator to make sure that sone object is never null or undefined .

It’s denoted by an exclamation mark.

For instance, we can write:

function bar(foo: Foo | undefined) {
  foo!.doSomething();
}

We have a foo parameter which can be undefined .

So to make sure it isn’t undefined , we use the !. operator to make sure that it’s not undefined .

No Magic Numbers

We should make sure that we don’t have magic numbers in our code.

They are hard to understand from the code.

Also, we’ve to change in multiple places if we have to change them.

Therefore, instead of writing:

const metersInFeet = meters * 3.28

We write:

const METER_IN_FEET = 3.28;
const metersInFeet = meters * METER_IN_FEET;

We create a constant and use that so everyone knows what it holds.

We can reference it anywhere so we only have to change it once if needed.

No Parameter Assignments

Assigning parameters is a bad idea.

It mutates the argument if it’s an object since they’re passed in by reference/

So instead of writing:

function foo(x: number) {
  x = 2;
}

We write:

function foo(x: number) {
  const y = 2;
}

No reference Syntax for Referencing Other TypeScript Files

Before ES modules are incorporated into TypeScript, we used the reference syntax to reference other TypeScript files.

However, now we can use ES6 modules instead.

For instance, instead of writing:

/// <reference path='./foo'>

We convert our TypeScript types to modules and write:

import baz from foo;

No var require

With TypeScript, we can use the import syntax to require CommonJS modules.

So instead of writing:

var module = require("module");

Instead, we write:

`import` module `= require('`module`');`

We use require , but with import on the left side.

It’s part of the language and TypeScript will compile this properly.

Arrow Functions

Whenever we don’t need this in our functions, we should use arrow functions.

They’re shorter and won’t bind to a value of this .

So instead of writing:

const foo = function() {
  doWork();
};

We write:

const foo = () => {
  doWork();
};

Use the for-of Loop

The for-of loop is the most versatile kind of loop in javaScript.

It lets us loop through any kind of JavaScript iterable object.

The only thing it can’t do is get us the index.

So when the index isn’t needed, we can write:

for (const a of arr) {
  const(a);
}

Use async with Promises

Functions that returns a promise should be marked async.

This way, we make sure that they return a promise with the resolved value.

For instance, instead of writing:

const foo = () => {
  return mypromise
    .then((val) => {
      return val;
    })
}

We write:

const foo = async () => {
  const val = await myPromise;
  return val;
}

They both return a promise with resolved value val .

Add Data Type Definitions to Our Code

We should add data type definitions to our code.

They can be added to variables, parameters, and as return types.

This will let us take advantage of data type checking and make them easier to read.

For instance, instead of writing:

function subtract(x, y) {
  return x - y;
}

We write:

function subtract(x: number, y: number): number {
  return x - y;
}

We specified the data types of the parameters and the return type.

Unifying Signatures

If we have 2 function signature overloads that can be combined into one with a union or rest parameter, we should do so.

For instance, if we have:

function foo(a: number);

function foo(a: string) {
  console.log(a);
}

We can write:

function foo(a: number | string) {
  console.log(a);
}

instead.

Conclusion

We can add non-null assertions to make sure something isn’t null or undefined .

Also, we shouldn’t use magic numbers.

And we can clean up our code with async and await , for-of loops, and combining signatures.

Categories
TypeScript Best Practices

TypeScript Best Practices — Strings and Regex

TypeScript is an easy to learn extension of JavaScript. It’s easy to write programs that run and does something. However, it’s hard to account for all the uses cases and write robust TypeScript code.

In this article, we’ll look at the best practices to following when writing code with TypeScript, including using includes over indexOf .

Also, we look at why we should use Regex.exec instead of String.match and why we should use the namespace keyword instead of using the module keyword to declare TypeScript modules.

Using includes Over indexOf

We should use includes over indexOf . It’s shorter and does the same thing.

There’s an includes method for both strings and arrays.

For instance, instead of writing:

const arr = [1, 2, 3];
const hasOne = arr.indexOf(1) !== -1;

or:

const str =  'foobar';
const hasFoo = str.indexOf(1) !== -1;

We write:

const arr = [1, 2, 3];
const hasOne = arr.includes(1);

or:

const str =  'foobar'
const hasFoo = str.includes(1);

includes finds check if an instance of something exists in a string or an array and returns true if it exists and false otherwise.

Require the use of the namespace Keyword Instead of module to Declare TypeScript Modules

To prevent any confusion with ES6 modules, we should use the namespace keyword to declare TypeScript modules.

For instance, instead of writing:

`module` Shapes {
  export class Circle { /* ... */ }
  export class Square { /* ... */ }
}

we write:

export namespace Shapes {
  export class Circle { /* ... */ }
  export class Square { /* ... */ }
}

Use Nullish Coalescing Operator Instead of Logical Chaining

Now that TypeScript has the nullish coaslescing operator, we should use that instead of logical chaining.

For instance, instead of writing:

function func(foo: string | null) {
  return foo || 'hello';
}

We should write:

function func(foo: string | null) {
  return foo ?? 'hello';
}

?? is better since it returns 'hello' only if foo is null or undefined .

Use Concise Optional Chain Expressions Instead of Chained Logical And

Optional chaining expressions are better than a long chain of logical && ‘s.

Therefore, we should use that instead of AND expressions.

For instance, instead of writing:

function bar(foo: T | null) {
  return foo && foo.a && foo.a.b && foo.a.b.c;
}

We write:

function bar(foo: T | null) {
  return foo?.a?.b?.c;
}

As we can see, it’s much shorter and they do the same thing.

Private Members Should be Marked as readonly if they’re Never Modified Outside of a Constructor

We should mark private members of a class as readonly if they’re never been changed outside of a constructor.

This way, we can prevent them from being changed accidentally anywhere else.

For instance, instead of writing:

class Foo {
  private foo: string;
  public constructor() {
    this.foo = "bar";
  }
}

We write:

class Foo {
  private readonly foo: string;
  public constructor() {
    this.foo = "bar";
  }
}

Now we can’t change this.foo anywhere in the class.

Use Type Parameter When Calling array.reduce Instead of Casting

If we use array.reduce with generic types, like an array or object, then we should specify a type parameter so we can specify a useful type for the initial value.

We can do that with a type parameter instead of a type assertion.

For instance, instead of writing:

[1, 2, 3].reduce((acc, num) => acc + num, 0 as number);

We write:

[1, 2, 3].reduce<number>((acc, num) => acc + num, 0);

Now we enforce the type on everything instead of just the second argument.

Use Regex.exec Instead of String.match if no Global Flag is Provided

Regex.exec is after than String.match and they both work the same if the /g flag isn’t added.

For instance, instead of writing:

'foobar'.match(/bar/);

We write:

/bar/.exec('foobar');

However, if we want to get all results of a pattern, then we’ve to use String.match :

'555-1212'.match(/d+/g);

Conclusion

We should use includes instead of indexOf to check if something is in a string or array.

Also, we should use Regex.exec as much as possible instead of String.match for finding single matches.

To declare a TypeScript module, use the namespace keyword instead of module .