- 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.
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:
// β
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:
// 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:
// β
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:
// β
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:
// β
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
anytype -
interfacesvstypes - Keyword
this -
generics