Categories
TypeScript Best Practices

TypeScript Antipatterns to Avoid

TypeScript is a language that extends the capabilities of JavaScript by adding type annotations to JavaScript code. This lets us avoid bugs from unexpected data types.

In this article, we’ll look at some antipatterns to avoid when writing TypeScript code.

Overusing the any Type

The point of using TypeScript is to have types in variables and functions. So we should use them wherever we can.

Therefore, we shouldn’t use the any type in most of our code.

Overusing Classes

If our TypeScript classes don’t have many methods, then we don’t need to define a class just to use it to type variables and functions.

Also, if we only have a single instance, then wrapping the logic within a class doesn’t make sense.

Instantiating classes introduce complex and it’s hard to optimize when minifying code.

Instead, we can define object literals and use interfaces or the typeof operator to get the type of an object.

For example, write the following interface for objects:

interface Person {
    firstName: string;
    lastName: string;
}

const person: Person = {
    firstName: 'Jane',
    lastName: 'Smith'
}

Then we can use them like we have above with person .

Also, if we don’t know the exact structure that the object will have, we can use typeof operator as follows:

const person = {
    firstName: 'Jane',
    lastName: 'Smith'
}

const person2: typeof person = {
    firstName: 'Joe',
    lastName: 'Smith'
}

Using typeof is a convenient way to define types without writing lots of code.

If our object has methods, we can do the same thing as above:

interface Person {
    firstName: string;
    lastName: string;
    fullName: (firstName: string, lastName: string) => string
}

const person: Person = {
    firstName: 'Jane',
    lastName: 'Smith',
    fullName(firstName: string, lastName: string) {
        return `${firstName} ${lastName}`;
    }
}

TypeScript will do type inference with typeof :

const person = {
    firstName: 'Jane',
    lastName: 'Smith',
    fullName(firstName: string, lastName: string) {
        return `${firstName} ${lastName}`;
    }
}
const person2: typeof person = {
    firstName: 'Joe',
    lastName: 'Smith',
    fullName(firstName, lastName) {
        return `${firstName} ${lastName}`;
    }
}

It’ll force us to add the fullName method to person2 if it’s missing, and it’ll force us to add the parameters and return a string.

Otherwise, we’ll get compiler errors.

Using the Function Type

The Function type is a generic type for functions. It’s like any for variables. We should specify the data types of parameters and return types in our functions.

Instead, we should add types to parameters and return types as follows:

type ArithmeticFn = (a: number, b: number) => number
const add: ArithmeticFn = (a: number, b: number): number => a + b;

We have the types in the type alias ArithmeticFn and also add . That’s enough type annotations in our code.

Also, once again, we can use typeof to do type inference:

const add = (a: number, b: number): number => a + b;
const subtract: typeof add = (a, b) => a-b

Then subtract also has the same parameter and return types as add .

Messing with Type Inference

We shouldn’t put useless type annotations with type inference will do the job.

For example, in the following example:

const courses = [{
    name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse: any = {...course};
newCourse.description = 'Great intro to TypeScript';
courses[0] = newCourse;

We have an extra any annotation that we shouldn’t have.

Instead, if we want to add a new property to an object after copying it, we can write the following:

const courses = [{
    name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse = {...course, description: 'Great intro to TypeScript'};
courses[0] = newCourse;

Then the TypeScript compiler won’t throw an error and we still get type inference from the TypeScript compiler since we didn’t use the any type.

Copying and Pasting Partial Type Definitions

Another thing that we shouldn’t do is copy and pasting partial type definitions from other places into our own code.

If we want to get the type of an object with an unknown type, we can use the typeof operator.

For example, we can write:

const person = {
    firstName: 'Joe',
    lastName: 'Smith',
    age: 20
}

const person2: typeof person = {
    firstName: 'Jane',
    lastName: 'Smith',
    age: 20
}

The TypeScript compiler will automatically recognize the type of person and will do checks when we define person2 to check if everything’s there.

Lookup Type

We can also use one property of an object as its own type. This is called a lookup type.

For example, we can write:

const person = {
    firstName: 'Joe',
    lastName: 'Smith',
    age: 20
}

const foo: typeof person.firstName = 'foo';

In the code above, TypeScript recognized person.firstName ‘s type is a string, so typeof person.firstName would be a string.

Mapped Types

We can create mapped types to map all properties of a type to something else.

For example, we can make all the properties of a type optional by writing:

interface Person{
    firstName: string;
    lastName: string;
}

type Optional<T> = {
    [P in keyof T]?: T[P];
};

const partialPerson: Optional<Person> = {};

The code above compiles and runs because we created a new type from Person where all the properties are optional.

Getting the Return Type of a Function

We can get the return type function with the ReturnType generic to pass in a type of the function. Then we’ll get the return type of that function.

For example, if we have:

const add = (a: number, b: number) => a + b;
const num: ReturnType<typeof add> = 1;

Then ReturnType<typeof add> will be number , so we have to assign a number to it.

TypeScript’s type inference is also working here. The ReturnType generic is available since TypeScript 2.8

Conclusion

In many cases, we can use the typeof operator, lookup types, or mapped types to annotate the types flexibly without losing type check capabilities.

This means we should eliminate the use of any as much as possible.

Also, we don’t need classes just for adding types to a few objects.

Categories
TypeScript Best Practices

TypeScript Best Practices — Interfaces vs Type Aliases and Unnecessary Syntax

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.

Class Name Casing

TypeScript class names should be PascalCase.

This also applies to interfaces.

So we write:

class Foobar {}

and:

interface FooBar {}

Use UTF-8 Encoding for Files

We should use UTF-8 encoding for files.

This way, there won’t be any issues using the file anywhere.

File Name Casing

We should name our files with a consistent case.

We can stick with camel case, Pascal case, kebab case, or snake case.

Camel case is fileName.ts .

Pascal case is FileName.ts ,

Kebab case is file-name.ts .

Snake case is file_name.ts .

We just stick with one for all files.

Use Explicit Increment or Decrement Operators

We should use explicit increment or decrement operators to make sure that we just assign the new value and don’t use the return value.

For instance, instead of writing:

++i;
i++;
--j;
j--;

which return the value and do the increment or decrement operation, we write:

i += 1;
i -= 1;
j += 1;
j -= 1;

Interface

We may want to have interfaces that start with I to distinguish them from other entities.

For instance, we may want to write:

interface IFoo {
  bar: string;
}

Use Interface Over Type Literal

Interfaces can be implemented, extended, and merged, so they’re preferred to type literals.

For instance, instead of writing:

type Alias = {
  num: number
}

We write:

interface Foo {
  num: number;
}

Match Default Export Name

If we have a default export, then our default import should match the name of the export.

This reduces confusion.

So if we have:

bar.ts

export default foo;

Then we write:

import foo from bar;

Newline Per Chained Call

If we have a chain of method calls, then we may want to put each of them in a new line so that it won’t overflow the page.

For instance, instead of writing:

foo.bar().baz();

We write:

foo
  .bar()
  .baz();

No Angle Bracket Type Assertion

We should use as for type assertions since we can use it for type assertions in .tsx files in addition to .ts files.

So instead of writing:

const foo = <Bar>bar;

We write:

const foo = bar as Bar;

No Boolean Literal Comparison

We shouldn’t compare with boolean literals since they’re redundant.

For instance, we can shorten:

if (x === true)

to:

if (x)

Use of Parameter Properties

We can have parameter properties in the TypeScript code.

They let us pass in constructor parameter and assign it to a value at the same time.

Instead of writing:

class Animal {
  constructor(private numLegs: number) {}
}

We can write:

class Animal {
  private numLegs: number = 2;
  constructor(numLegs: number) {
    this.numLegs = numLegs;
  }
}

No Reference Import

We shouldn’t use reference if we use import .

For instance, if we have:

<reference path="foo.bar" />

We write:

import { bar } from 'foo';

It’s more standard and we don’t need reference to pull type definitions out of type definition files anymore.

No Useless Callback Wrappers

We shouldn’t have useless callback wrappers in our code.

For instance, instead of writing:

const handleContent = (content) => console.log('do something with', content);

promise.then((content) => handleContent(content))

We write:

const handleContent = (content) => console.log('do something with', content);

promise.then(handleContent)

It’s a lot cleaner.

No Unnecessary Initializers

We shouldn’t have to set variables to undefined .

For instance, instead of writing:

let x =  undefined;

We write:

let x;

No Object Literal Key Quotes

If an object literal has valid identifiers as property names, then we don’t need quotes around the names.

For instance, instead of writing:

const obj = { 'foo': 1 };

We write:

const obj = { foo: 1 };

Conclusion

We should have a consistent file name casing.

Also, we shouldn’t have unnecessary syntax in our code.

Imports should be used to pull data type definitions instead from external sources.

Useless callback wrappers should also be removed.

Interfaces should be used instead of type alias since they’re more versatile.

Categories
TypeScript Best Practices

TypeScript Best Practices — Namespace, any, for-in and More

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.

Group Function Overloads Together

We can overload functions.

This means we can have multiple function signatures for a function, which TypeScript can check for.

To make our lives easier, we should group them together so that it’s easier to read:

function foo(a: string, b: string): string;

function foo(a: number, b: number): number;

function foo(a: any, b: any): any {
  console.log(a, b);
}

Visibility Declarations for Class Members

To take advantage of the access control capabilities of TypeScript, we can add the visibility declarations or class members.

For instance, we write:

class Employee {
  private getSalary(): number {
    return 90000;
  }
}

We added the private access modifier so that getSalary can only be called by other methods in the class.

There’s also the public modifier to make the member available to outside code.

protected makes the member available to subclasses and the current class.

public is the default.

We can also do the same for instance variables:

class Employee {
  private empCode: number;
}

Ordering Class Members

We may consider ordering class members to make the members easier to read.

We can order them by access modifiers, alphabetical order, etc.

It’s good to stick with one.

For instance, instead of writing:

class Employee {
  private id: string;
  private empCode: number;
  private empName: string;
}

We write:

class Employee {
  private empCode: number;
  private empName: string;
  private id: string;
}

to sort them by alphabetical order.

Eliminate the Use of any Types

We can eliminate the use of any types in our code to take advantage of TypeScript’s type-checking capabilities.

If we need something more flexible than static types, there are many ways to define them.

We can use literal types to restrict values to only some literals.

There’re union types to let us check for members for multiple types.

The intersection type makes sure that the variable has members that are in both types.

Index signatures let us check for dynamic properties.

There’re many ways to avoid any .

For instance, instead of writing:

let bar: any;

We write:

let foo: string;

No Empty Interface

We shouldn’t have empty interfaces in our code since they’re useless.

So instead of writing:

interface I {}

We write:

interface I {
  bar: number;
}

No for-in Loops

for-in loops are legacy JavaScript syntax which has better modern alternatives.

It’s bad since we need to use the hasOwnProperty to check for non-inherited properties with it.

Better alternatives include Object.keys to get the non-inherited keys of an object.

Object.values to get the values and Object.entries to get all entries.

So instead of writing:

for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(obj[key]);
  }
}

We write:

for (const key in Oject.keys(obj)) {
  console.log(obj[key]);
}

No Import Side Effects

import should be used for import module members and using them.

If they perform side effects, then it’s not good because it’s hard to statically analyze the code.

So instead of writing:

import 'foo';

We write:

import { bar } from 'foo';

Some exceptions may be importing CSS with Webpack as modules.

So we may still write:

import 'styles.css';

No Explicit Type Declarations for Variable or Parameters with Literal Values

It’s redundant to have type declarations for variables or parameters that are assigned with numbers strings or boolean.

TypeScript can check these without an explicit type.

Therefore, instead of writing:

const foo: number = 10;

We write:

const foo = 10;

Don’t Use module Keyword for Namespaces

If we declare namespaces, then we should use the namespace keyword.

Instead of writing:

module Math {
  function add(a: number, b: number): number {
    return a + b;
  }
}

We write:

namespace Math {
  function add(a: number, b: number): number {
    return a + b;
  }
}

This way, we won’t confuse what we have with ES modules.

Conclusion

Grouping things together make them easier to read.

Visibility modifiers are a useful TypeScript feature.

any types can be replaced with many things.

Use namespace to declare namespaced code.

Categories
TypeScript Best Practices

TypeScript Best Practices — Function and Array Types, Reducing Complexity

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.

Use isNaN to Check for NaN

NaN doesn’t equal itself, so we can’t use === or !== to check for NaN .

Instead, we use the isNaN function to check for it.

So instead of writing:

if (foo === NaN) {}

We write:

if (isNaN(foo)) {}

Invalid Use of void

We should watch for invalid uses of void .

We shouldn’t mix void with other types since void means nothing rather than an explicit type of value.

If we need undefined , then we should use the undefined type.

So types expressions like:

Foo | void

should be replaced with:

Foo | undefined

Max Number of Classes Per File

We shouldn’t have too many classes per file.

It’s probably too complex if we do.

We can set the max number of classes per file with a linter.

Max File Line Count

If a file has too many lines, then it’s probably too complex and hard to read.

We can set the max file line count in a file with a linter.

Default Exports

Default exports aren’t as clear, so we should avoid using them.

Named exports are clearer.

So we may want to avoid them.

Default Import

Like default exports, default imports aren’t as clear as named imports.

No Duplicate Imports

We can combine multiple import statements from the same module into one import statement, so we should do that.

For instance, instead of writing:

import { foo } from 'module';
import { bar } from 'module';

We write:

import { foo, bar } from 'module';

No Mergeable Namespace

We shouldn’t have 2 namespaces with the same name so we don’t have to think about how they’ll be merged together.

Instead, we can just name different namespaces with different names.

For instance, instead of writing:

namespace Animals {
  export class Dog {}
}

namespace Animals {
  export interface Cat {
    numberOfLegs: number;
  }
  export class Zebra {}
}

We write:

namespace Pets {
  export class Dog {}
  export interface Cat {
    numberOfLegs: number;
  }
}

name WildAnimals {
  export class Zebra {}
}

No require Imports

Now that ES modules are widely available, we can replace require with import .

For instance, instead of writing:

const { bar } = require('foo');

We can use the ES6 module version of the module and write:

import { bar } from 'foo';

Use const

We should use const instead of let or var whenever we can for declaring variables.

This way, they can’t be accidentally be assigned with another value.

For instance, instead of writing:

var x = 1;
let y = 2;

We write:

const z = 3;

Marking Variables as readonly

We should mark variables as readonly if they’re private and are never modified outside of the constructor.

This way, we won’t be able to accidentally change them.

For instance, instead of writing:

class Foo {
  private bar = 1;
}

We write:

class Foo {
  private readonly bar = 1;
}

Arrow return Shorthand

If we have an arrow function that only has one statement and returns something, we can make them shorter.

Instead of writing:

() => { return x; }

We write:

() => x

Set Array Types

We should set the types of the array content so that we can restrict the types of values that go into the array.

We can also set generic types to make the array generic.

For instance, instead of writing:

const arr = ['foo', 'bar', 1];

We write:

const arr: string[] = ['foo', 'bar', 'baz'];

Callable Types

If we have an interface or a literal type with just a call signature, then we should write it as a function type.

For instance, instead of writing:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

We write:

(source: string, subString: string) => boolean

Conclusion

We should use function types instead of interfaces or literal types with just a function signature.

Arrays should have a type so we can’t put any type of data in it.

We should reduce the complexity of each file.

isNaN should be used for checking for NaN .

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.