Published on

TypeScript: Top 5 common pitfalls developers face

Table of Contents

1. Strict Null Checks - strictNullChecks

Problem Description:

The strictNullChecks option in TypeScript tsconfig ensures that variables are initialized to a non-null value, which helps prevent common runtime errors. If this option is not enabled, it can lead to unexpected behavior and errors.

Example: Only when we enable this option will raise an error that you have not make sure the departures is not undefined before using it.

ts-strict-null-checks.tsx
declare const targetCountry: string

const flights = [
  { from: 'LCA', to: 'ATH' },
  { from: 'AMD', to: 'VIE' },
]

const departures = flights.find((fl) => fl.from === targetCountry)
console.log(departures.to) //🚫 ERROR: Object is possibly 'undefined'.

Solution:

Enable strictNullChecks by setting the "strict" option in your tsconfig.json file to true, or adding the --strictNullChecks flag when compiling your TypeScript code. For example:

{
  "compilerOptions": {
    "strict": true,
    "target": "es6",
    "module": "commonjs",
    "sourceMap": true
  }
}

2. Misusing any type

The any type in TypeScript allows for dynamic typing, but can be misused and lead to runtime errors. If used excessively, it can also negate many of the benefits of using TypeScript.

Solution: Avoid using the any type unless necessary, and use specific types whenever possible. If a variable's type cannot be determined at compile-time, consider using a union type instead of any.

Example:

ts-misusing-any.tsx
// ❌
const handleAddOne = (a: any, b: any): any => {
  return a + b
}

// βœ…
const handleAddTwo = (a: number | string, b: number | string): number | string => {
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b
  } else {
    return `${a}${b}`
  }
}

3. Not understanding the difference between interfaces and types

Interfaces and types are similar in TypeScript, but have different use cases. Using the wrong one can lead to unexpected behavior or errors.

Solution: Use interfaces when defining the shape of an object or class, and types for defining aliases for specific types.

Example:

ts-interface-vs-type.tsx
// Interface
interface User {
  firstName: string
  lastName: string
  role: string
}

// Type alias
type UserAlias = {
  firstName: string
  lastName: string
  role: string
}

4. Using incorrectly the keyword this

The this keyword in TypeScript can be used to refer to the current object, but can be misused if the context is not properly set. This can lead to runtime errors.

Solution:

Use arrow functions or bind the this context explicitly to avoid errors related to this.

Example:

ts-keyword-this.tsx
// ❌
class User {
  private id: string

  constructor(id: string) {
    this.id = id
  }

  updateUser() {
    setTimeout(function () {
      console.log(this.id) //🚫 Potentially invalid reference access to a class field via 'this.' of a nested function
    }, 1000)
  }
}

// βœ…
class User {
  private id: string

  constructor(id: string) {
    this.id = id
  }

  updateUser() {
    setTimeout(() => {
      console.log(this.id)
    }, 1000)
  }
}

Note: Here in the first example this refers to the anonymous function that's wrapped where in the second example the arrow function eliminates such mislead

4. Not using generics correctly

Generics allow us to write reusable code in TypeScript, but if are not been used correctly, can lead to errors.

Solution: We can use generics to define reusable code that can work with multiple types. Type constraints should be defined for the generic type to ensure that the correct types are been used.

Example:

ts-generics-wrong.tsx
// ❌
class ShoppingCart<T> {
  private items: T[]

  constructor() {
    this.items = []
  }

  public addItem(item: T) {
    this.items.push(item)
  }

  public getItems(): T[] {
    return this.items
  }
}

const myCart = new ShoppingCart<string>()
myCart.addItem('item1')
myCart.addItem('item2')

myCart.addItem(22) //🚫 No ERROR is thrown and Type check was not present

console.log(myCart.getItems())

In this scenario, we have a generic class ShoppingCart that has a private array of items of type <T>. The class has two methods: addItem which adds an item to the array, and getItems which returns the array of items.

However, the addItem method does not enforce that the item being added is of the same type as the items already in the array, which can lead to unexpected behavior.

To properly use generics, we need to add a constraint to ensure that the items being added to the array are of the same type as the items already in the array:

ts-generics-proper.tsx
// βœ…
class ShoppingCart<T> {
  private items: T[]

  constructor() {
    this.items = []
  }

  public addItem(item: T) {
    if (typeof item === typeof this.items[0] || this.items.length === 0) {
      this.items.push(item)
    } else {
      throw new Error('Invalid item type')
    }
  }

  public getItems(): T[] {
    return this.items
  }
}

const myCart = new ShoppingCart<string>()
myCart.addItem('item1')
myCart.addItem('item2')

myCart.addItem(22) //🚫 ERROR is thrown as expected

console.log(myCart.getItems())

In this second scenario, we're enforcing that the item being added to the array is of the same type as the items already in the array, or that the array is empty. If the item being added is of a different type than the items already in the array, we throw an error. Now, the code will correctly throw an error when we try to add a number to an array of strings.

Summary:

we have seen :

  • strictNullChecks
  • Misusing any type
  • interfaces vs types
  • Keyword this
  • generics