Categories
TypeScript Best Practices

TypeScript Best Practices — Comparisons, Useless Types, and any

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 disallowing the use of useless comparisons.

Also, we don’t want to write conditionals for things that are always truthy or falsy.

Useless namespace or enum qualifiers can also be removed.

And we should add type annotations to functions before calling them.

Don’t do Equality Comparison Against Boolean Literals

We shouldn’t do equality comparisons against boolean literals.

We can omit it and get the same result.

For instance. the following:

if (someCondition === true) {
}

is the same as :

if (someCondition) {
}

and:

if (someCondition === false) {
}

is the same as:

if (!someCondition) {
}

So we shouldn’t compare against boolean literals.

Don’t Write Conditionals that are Always Truthy or Falsy

We shouldn’t write conditionals that are always truthy or falsy.

For instance, we shouldn’t have code like:

const foo = (arg: 'baz' | 'bar') => {
  if (arg) {
  }
}

because arg is either 'bar' or 'baz' , so it’s always truthy.

In addition to if , this also applies to for , while and do-while statements and base values of optional chaining expressions.

For instance, if we have:

const foo = (arg: string) => {
  return arg?.length;
}

Since arg can never be bullish, so we don’t need the ? .

No Useless Namespace Qualifiers

We shouldn’ write useless namespace or enum qualifiers.

For instance, instead of writing:

namespace A {
  export type B = number;
  const x: A.B = 3;
}

We write:

namespace A {
  export type B = number;
  const x: B = 3;
}

since B is in A , we don’t need to specify A .

Likewise, for enums, instead of writing:

enum A {
  B,
  C = A.B,
}

We write:

enum A {
  B,
  C = B,
}

Again, B is in A , so we don’t have to write A .

Remove Type Arguments that aren’t Used

We may want to remove explicit type arguments that are the same as the default.

This is because they’re redundant.

For instance, instead of writing:

function foo<T = number>() {}
foo<number>();

We just write:

function foo<T = number>() {}
foo();

Likewise, for function signatures, instead of writing:

class C<T = number> {}
function bar(c: C<number>) {}

We can write:

class C<T = number> {}
function bar(c: C) {}

Don’t Write Useless Type Assertions

If a type assertion doesn’t change the type of an expression, then we shouldn’t write it.

For instance, instead of writing:

const foo = 5;
const bar = foo!;

or:

const foo = <3>3;

or:

const foo = <3>3;

We should write:

const foo = <number>3;

or:

const foo = 3 as number;

or:

const foo = 3 as const;

Changing 3 to the literal type 3 doesn’t change the type.

But changing 3 to the type number does change the type by making it broader.

Don’t Assign any to Variables and Properties

We shouldn’t change the type of any value to any or any[] .

They bypass TypeScript type checks so makes our TypeScript code less robust.

Therefore, instead of writing:

const x = 1 as any;

or:

const [x] = [1] as any[];

We write:

const x = 1 as number;

or:

const [x] = [1] as number[];

Don’t Call any Type Value

We may also look out for any properties or other nested locations.

For instance, instead of writing:

declare const nestedAny: { prop: any };
nestedAny.prop.a();

We may write:

declare const nestedAny: { prop: : { a: () => void } };

Now we can call:

nestedAny.prop.a();

safely since we know nestedAny.prop.a must be a function.

Conclusion

Writing conditionals for things that are always truthy or falsy are redundant, so we should remove it.

Also, useless type assertions that don’t change the type of value should be removed or changed.

Comparing against boolean literals are also redundant so we should also remove those.

Categories
TypeScript Best Practices

TypeScript Best Practices — Namespaces, Exceptions, and Type Definitions

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 disallowing the use of TypeScript modules and namespaces.

Also, we don’t need non-null assertions after optional chaining expressions. If we create and use modules, we should use JavaScript modules. Instead of throwing literals, we should throw exceptions.

And, we may want to use one way to declare types instead of 2.

Don’t use Custom TypeScript Modules and Namespaces

Since we have ES6 modules as a standard in JavaScript, we don’t need custom TypeScript modules and namespaces to organize our code.

Instead, we should use standard JavaScript modules with import and export instead.

For instance, instead of writing:

module foo {}
namespace foo {}

or:

declare module foo {}
declare namespace foo {}

We write:

export default foo;

This exports the foo object from a module.

We can also export individual members of a module:

export foo;
export bar;

Don’t Use Non-Null Assertion After Optional Chain Expression

We shouldn’t use non-null assertions after an optional chain expression since they’re opposites of what the optional chain expression does.

By its nature, the expression can return undefined.

For instance, instead of writing:

foo?.bar!;

or:

foo?.bar()!;

We write:

foo?.bar;

or:

foo?.bar();

Don’t Use Non-Null Assertions Using the ! Postfix Operator

Non-null assertions cancel the benefits of strict null-checking mode.

Therefore, we may want to remove the extra ! operators.

For instance, instead of writing:

interface Foo {
  bar?: string;
}

const includesBaz: boolean = foo.bar!.includes('qux');

Instead, we should write:

interface Foo {
  bar?: string;
}

const hasQux: boolean = foo.bar && foo.bar.includes('qux');

Don’t Use Parameter Properties in Class Constructors

Parameter properties are confusing to people new to TypeScript, so we may want to stop the use of that.

It’s a less explicit way to declare and initialize class members.

For instance, instead of writing:

class Foo {
  constructor(readonly name: string) {}
}

We write:

class Foo {
  constructor(name: string) {}
}

Don’t use require() to Import Modules

Now that ES6 modules are standard, we don’t have to use require to import CommonJS modules anymore.

Therefore, instead of writing:

const lib = require('lib');

We write:

import { foo } from 'lib';

Don’t Alias this

Now that we have arrow functions, we don’t need to set this to another variable to keep its value.

For instance, instead of writing:

const self = this;

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

We should write:

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

Don’t Throw Literals as Exceptions

We should throw Error objects with throw instead of literals since it gives us more information like the line an exception occurred, the stack trace, and the type of error.

For instance, instead of writing:

throw 'error';

We write:

const err = new Error();
throw err;

or:

class BadError extends Error {
  // ...
};
throw new BadError();

Don’t use Type Aliases

We may want to disallow the use of type alias.

Type aliases can alias other types so that we can refer them with simpler names.

For instance, we can write:

type Person = {
    firstName: string,
    lastName: string,
    age: number
};

let person: Person;

instead of:

let person: {
    firstName: string,
    lastName: string,
    age: number
};

It can also act as an interface, like the example before.

Or it can act as a mapping too to let us do quick modifications.

For example, we can write:

type ReadOnly<T> = { readonly [P in keyof T]: T[P] };

type Person = {
  firstName: string;
  lastName: string;
  age: number;
};

For cases where they act like interfaces, maybe we just want to convert them to interfaces instead.

This way, we get one kind of type annotations instead of 2 in our code.

Conclusion

We may want to stick with interfaces for declaring types if our type alias is used like interfaces.

Also, now that we have arrow functions, we don’t need an alias for this .

Instead of throwing literals, we should throw Error objects when we raise exceptions.

Categories
TypeScript Best Practices

TypeScript Best Practices — Iteration, Promises, and eval

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 replacing for-in loops with better alternatives.

Promise code should also be useful.

eval-like Methods also shouldn’t be used.

No Iteration Over an Array with a for-in Loop

The for-in loop isn’t all that useful now that there are the for-of loops and methods to get object keys.

Also, it iterates over the prototypes of our object, which we probably don’t want.

The order of iteration also isn’t guaranteed.

Therefore, we shouldn’t use it in our code.

Instead of writing:

for (const x in [1, 2, 3]) {
  console.log(x);
}

We write:

for (const x of [1, 2, 3]) {
  console.log(x);
}

The for-in loop loops over the indexes, while the for-of loop loops over the entries.

Don’t Use eval-Like Methods

In JavaScript and by extension, TypeScript, both have methods that take strings and run them as code.

There’s the eval method which runs code from a string.

The Function constructor also returns a function from a string.

Also, setTimeout and setInterval functions can both run code from a string.

This prevents any optimization from being done since the code is in a string.

Also, it creates a big security flaw since anyone can input a string with code and run it potentially.

Therefore, we shouldn’t run any function that allows us to run code from string.

If we use setTimeout or setInterval , we should pass in a callback instead of a string.

For instance, instead of writing:

setTimeout('alert(`foo`);', 100);

or:

const add = new Function('a', 'b', 'return a + b');

We write:

setTimeout(() => {
  alert(`foo`);
}, 100);

or:

setInterval(() => {
  alert(`foo`);
}, 10000);

and don’t use the other functions.

No Explicit Type Declarations for Variables or Parameters Initialize to a number, string, or boolean

We don’t need types for anything that’s been explicitly assigned a number, string, or boolean.

This is because it’s obvious from the code what they are.

Therefore, instead of writing:

const a: number = 10;

We write:

const a = 10;

This applies to any other primitive values like string or boolean.

If the return type is obvious, then we can also skip the type annotation.

So, instead of writing:

const a: number = Number('1');

We write:

const a = Number('1');

This also applies to regexes. If we have a regex literal, then we don’t nee the annotation.

Instead of writing:

const a: RegExp = /foo/;

We write:

const a = /foo/;

Use new and constructor in Valid Ways

We should use new and constructor in valid ways.

Therefore, we shouldn’t use them for creating new class instances or as constructor functions respectively.

For instance, instead of writing:

class C {
  new(): C;
}

We write:

class C {
  constructor(){
    //...
  }
}

const c = new C();

Constructors should only be in classes.

We can also specify new as a signature in interfaces:

interface I {
  new (): C;
}

Don’t Use Promises in Places that aren’t Designed to Handle Them

We shouldn’t use promises in places that aren’t designed to handle them.

For instance, we shouldn’t put them in if statements or loops.

So instead of writing:

const promise = Promise.resolve('foo');

if (promise) {
  // Do something
}

or:

const promise = Promise.resolve('foo');

while (promise) {
  // Do something
}

or:

[1, 2, 3].forEach(async value => {
  await foo(value);
});

or:

new Promise(async (resolve, reject) => {
  await doSomething();
  resolve();
});

We should write:

const promise = Promise.resolve('foo');

if (await promise) {
  // Do something
}

or:

const promise = Promise.resolve('foo');

while (await promise) {
  // Do something
}

or:

for (const value of [1, 2, 3]) {
  await foo(value);
}

We need to put await in the right places so that we get the resolved value properly.

Also, the following isn’t valid:

new Promise(async (resolve, reject) => {
  await doSomething();
  resolve();
});

because it’s redundant to have a promise inside a promise.

We can move the await doSomething() outside our promise callback.

Conclusion

Promises should be used in a useful manner or they shouldn’t be used at all.

eval-like functions shouldn’t be used because they’re dangerous.

for-in loops should be replaced with better alternatives.

Categories
TypeScript Best Practices

TypeScript Best Practices -Useless Interfaces, Classes, and Promises

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 not writing empty interfaces.

Also, we should use promises in a useful way.

The any type also shouldn’t be used.

Also, classes shouldn’t be used as namespaces in TypeScript.

No Empty Interface Declarations

Empty interfaces aren’t very useful. Therefore, we probably don’t want them in our code.

So instead of writing:

interface Foo {}

or:

interface Baz extends Foo {}

We write:

interface Foo {
  name: string;
}

or:

interface Baz extends Foo, Bar {}

Extending 2 types inherit members from both types, so that may be useful.

Don’t use the any Type

The any type allows anything to be assigned to a variable in TypeScript.

Therefore, using the any type will bypass type checking for anything with that type.

To maximize the usefulness of the TypeScript compiler, we should add some types.

So instead of writing:

const foo: any = 'bar';

We can write:

const foo: string = 'bar';

Likewise, for arrays, if we have:

const arr: any[] = [1, 2];

We should write:

const arr: number[] = [1, 2];

instead.

For parameters, we write:

function greet(names: Array<string>): string {}

instead of:

function greet(names: Array<any>): string {}

We can make our types dynamic with union types, intersection types, and index signatures.

These are much better choices than using any .

No Extra Non-Null Assertion

We shouldn’t have non-null assertion that doesn’t add any extra value in our code.

For instance, if we have:

const bar = foo!!!.bar;

or:

function baz(bar?: { n: number }) {
  return bar!?.n;
}

Then the ! or there’s a nullable operator which cancels out the non-null operator or they’re redundant.

Instead, we should make our non-null operator useful by writing:

const bar = foo!.bar;

Don’t Use Classes as Namespaces

Classes shouldn’t be used as namespaces.

In TypeScript, we can put code other than classes at the top-level, so we can just do that instead of wrapping them in a class.

For instance, instead of writing:

class Foo {
  constructor() {
    foo();
  }
}

or:

class AllStatic {
  static num = 42;
  static greet() {
    console.log('Hello');
  }
}

We can write:

class Foo {
  constructor() {
    foo();
  }

  bar(){}
}

or:

class Bar {
  num = 42;
  greet() {
    console.log('Hello');
  }
}

We should have some instance methods or fields so that we actually need to create a class.

If we only have static variables, then we should create an object literal.

If we have an empty class or a class with only a constructor, then we don’t need the class.

Require Promise-Like Values to Be Handled Properly

We should handle promise-like values properly.

So if we have a promise in our code, we should use await in an async function and catch block to catch errors.

Or we call then on a promise, then we should call catch and/or finally on it to catch errors.

For instance, instead of writing:

const promise = new Promise((resolve, reject) => resolve('foo'));

or:

const foo = async() => {
  return 'value';
}

or:

Promise.reject('error').catch();

or:

Promise.reject('error').finally();

We should write:

const promise = new Promise((resolve, reject) => resolve('value'));
await promise;

or:

const foo = async () => {
  return 'value';
}
foo().then(
  () => {},
  () => {},
);

or:

Promise.reject('error').catch(() => {});

or:

Promise.reject('error').finally(() => {});

If our promise returns a value by resolving or rejecting, then we should handle them.

Otherwise, we may not need the promise to be present in our code.

Conclusion

We should use promises in a useful way in our code. Otherwise, we should remove them.

Classes shouldn’t be used as namespaces since we can put code at the top level.

Finally, we shouldn’t have empty interfaces in our code.

Categories
TypeScript Best Practices

TypeScript Best Practices — Delimiters and Ordering

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 delimiters for interfaces and type literals.

Also, we’ll look at consistency in declaration order, useless calls to toString , and bad uses the delete operator.

Require Specific Member Delimiter Style for Interfaces and Type Literals

In TypeScript, interfaces and type literals can have members separated by a comma or a semicolon.

It’s a good idea to have a consistent style.

For instance, we can write them all as follows:

interface Foo {
  name: string;
  bar(): void;
}

or we can write as follows:

interface Foo {
  name: string,
  bar(): void,
}

For type literals, we can write:

type Foo {
  name: string;
  bar(): void;
}

or:

type Foo {
  name: string,
  bar(): void,
}

They’re both valid, but TypeScript prefers semicolon.

Require a Consistent Member Declaration Order

It may be a good idea to order our members in interfaces, type literals, classes, and class expressions in a certain way.

This way, it’s easier to read as similar items are grouped together.

We can group the public and static fields together.

Or we can group protected and static fields together, for example.

Alternatively, we can group them alphabetically.

For instance, we can write:

class Foo {
  private foo: string;
  public bar: string;
  protected static baz: string;

  constructor() {}

  public static A(): void {}
  public B(): void {}

  [Z: string]: any;
}

We grouped the fields together at the top. Then we have the constructor.

Next, we have public methods.

Then we have the index signature to allow us to put in anything else.

Likewise, we can do the same for type literals:

interface Foo {
  [Z: string]: any;

  bar(): void;

  new ();

  baz: string;
}

We have our index signature, then a method, then the constructor, and finally the field.

These are just some possibilities for organizing members.

Enforce Particular Method Signature Syntax

We can enforce the syntax of method signatures in types and interfaces.

For instance, we can write:

interface Foo {
  foo(arg: string): number;
}

or we can write:

interface Foo {
  foo: (arg: string) => number;
}

We just have to keep one consistent type for better readability.

Enforce Naming Convention for Everything Across a Codebase

To make our code more predictable, we can enforce a naming convention across a whole project.

We can validate leading underscores, trailing underscores.

Also, we can check prefixes and suffixes.

The format can also be checked.

For casing, we should stick with the convention specified in JavaScript.

Therefore, we should stick with camelCase for variables and functions.

Constructor functions or classes should be PascalCase.

Constants that don’t change should be in upper case with underscores separating the words.

Parameters are also camelCase.

Enum cases are upper case while the name is PascalCase.

toString Should Only be Called on Objects to Provide Useful Information When Stringified

We should call toString to get some useful information on objects.

Therefore, if our object only inherits from Object ‘s prototype’s toString method, then we shouldn’t call it.

For instance, if we have an empty object, then we shouldn’t call toString on it, since it doesn’t give us anything useful:

{}.toString()

However, if our object has its own toString method:

const foo = {
  toString() {
    return "foo";
  }
};

const str = foo.toString();

then we can call it.

Disallow the delete Operator with Computed Key Expressions

Since deleting dynamically computed keys can be dangerous, we may want to not write that co0de.

For instance, we can write:

delete foo[prop];

and prop can be anything, so we may delete the property that we want to keep.

Conclusion

We may not want to use the delete operator to delete dynamic properties because we may delete the wrong thing.

Also, since Object ‘s prototype’s toString isn’t all that useful, we shouldn’t call it.

Having specific orders of members for literals may be good to make looking things up easier.

Also, method signature syntax may be written in a specific order for predictability.