Learning

10 Common Mistakes in Typescript Development

clock-icon

7 minutes read

writer-avatar

Radu Dumitrescu

Nov 16, 2023

Share on:

linkedin-icon

We will walk through the most common 10 mistakes made in the TypeScript world, hoping to write cleaner, more reliable code.

Below are the subjects we will address:

  1. Overuse of type “any”
  2. Misusing “as” Casts
  3. Ignoring Type-check Errors and Warning
  4. Not Using Strict Mode
  5. Implicit “any” in Function Return Types
  6. Not using “const” and “read-only” correctly
  7. Inconsistent Coding Conventions
  8. Overcomplicating Types
  9. Not Leveraging TypeScript Features
  10. Using third-party libraries

1. Overuse of type any

One of the most common mistakes when transiting from a non typed language to a typed one is using any type excessively. While any can come in handy in certain situations, relying on it too heavily undermines TypeScript’s benefits. It’s essential to use more specific types whenever possible to catch potential bugs early in the development process.

Here are two code examples illustrating the overuse of any type and how it can lead to issues in TypeScript:

Overusing any in Function Declarations:

function add(a: any, b: any): any {
  return a + b;
}

const result = add(5, "10"); // No TypeScript error, logic error
console.log(result); // Outputs "510" instead of 15

In this example, the add function uses any type for its parameters and return type. While TypeScript doesn’t complain, it allows for adding up two values of different types (number and string), which results in an unexpected behavior.

Overusing any in Arrays:

const data: any[] = [1, "two", true, { value: 4 }];

for (const item of data) {
  // No type-checking here
  console.log(item.value);
}

data.push(null); // No TypeScript error, but it can lead to issues later

Here an array data is declared with the type any[], which means it can contain elements of any type. While this may be convenient in the short term, it bypasses TypeScript’s type-checking for array elements, potentially leading to runtime errors or unexpected behavior when working with the array’s contents.

2. Misusing as Casts

The as keyword allows you to assert a type for a variable, but it should be used with caution. Misusing as casts can lead to runtime errors if the assertion is incorrect. It’s better to let TypeScript infer types whenever possible and use as only when you have strong reasons to do so, such as when dealing with third-party libraries or complex type transformations.

let userInput: any = "Hello, TypeScript!";
let strLength: number = (userInput as string).length;

console.log(strLength); // Outputs: 18

While this code works in this particular scenario, it’s considered a misuse of as casts because we are bypassing TypeScript’s type-checking. If userInput were not a string, a runtime error would occur.

3. Ignoring Type-check Errors and Warning

TypeScript provides valuable type-checking and inference, but some developers ignore or suppress type-check errors and warnings using the //@ts-ignore comment. Ignoring these messages may hide potential issues that could lead to runtime errors or unexpected behavior. It’s crucial to address type-check errors and warnings systematically to ensure code correctness.

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

//@ts-ignore
const result = add(5, "10"); // No type error suppressed
console.log(result); // Outputs "510" instead of 15

4. Not Using Strict Mode

TypeScript’s strict mode enables a set of stricter type-checking options, including noImplicitAny, strictNullChecks. If you don’t enable strict mode in your TypeScript configuration can result in less robust code. Enabling strict mode helps catch more type-related issues at compile-time and promotes safer coding practices.

5. Implicit any in Function Return Types

When TypeScript cannot infer the return type of a function, it defaults to any. This can lead to unexpected behavior and null / undefined related errors. Always explicitly declare function return types to ensure that TypeScript provides accurate type-checking and to make your code more predictable.

6. Not using const and read-only correctly

TypeScript supports const assertions and readonly properties to enforce immutability. Failing to use these features correctly can result in unintentional mutations and errors. Utilize “const” and readonly appropriately to ensure that your data remains consistent and your code behaves as expected.

Below are some examples of incorrect usage of const and readonly:

Incorrect Usage of const with Arrays

const numbers = [1, 2, 3];

numbers.push(4); // No TypeScript error, but the array is modified.
numbers = [4, 5, 6]; // Error: Cannot assign to 'numbers' because it is a constant.

In this example, const is used to declare an array of numbers. While attempting to reassign the entire array results in an error, modifying the array using push does not produce a TypeScript error, even though it changes the array’s content.

Misusing readonly with Class Properties

class Circle {
  readonly radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  setRadius(newRadius: number) {
    // Error: Cannot assign to 'radius' because it is a read-only property.
    this.radius = newRadius;
  }
}

In this example, the readonly keyword is used correctly to declare a read-only property radius in the Circle class. However, the setRadius method attempts to reassign the radius property, which is not allowed.

7. Inconsistent Coding Conventions

Inconsistent coding conventions, such as using different naming conventions for variables or not following a consistent folder structure, can make your TypeScript project challenging to maintain. Establish clear coding conventions and adhere to them to enhance code readability and better collaboration among team members.

// Inconsistent variable naming
const UserName = "John";
const user_age = 25;
const userAddress = "123 Main St";

// Inconsistent indentation
function greet() {
  console.log("Hello,");
  console.log("World!");
}

// Inconsistent use of quotes
const message = "Hello, TypeScript!";
const title = "Welcome to the TypeScript tutorial.";

// Inconsistent bracing style
if (condition) {
  console.log("This uses the 'K&R' style.");
} else {
  console.log("This uses a different style.");
}

// Inconsistent file naming
-userManagement.ts - User - Service.ts - userStore.ts;

8. Overcomplicating Types

While TypeScript allows you to create complex types, overcomplicating them can lead to code that is challenging to understand and maintain Look for a balance between precision and simplicity in your type definitions, and consider using type aliases and interfaces to make your code more accessible.

Some examples of overcomplicated types:

Excessive Use of Mapped Types

type OriginalData = {
  name: string;
  age: number;
  city: string;
};

type MakeReadOnly<T> = {
  readonly [K in keyof T]: T[K];
};

const readOnlyData: MakeReadOnly<OriginalData> = {
  name: "Alice",
  age: 30,
  city: "New York",
};

In this example, a mapped type MakeReadOnly is used to make all properties of a type readonly. While mapped types can be useful, excessive use of them can lead to code that is difficult to understand and maintain.

Complex Conditional Types

type Vehicle = {
  type: "car" | "motorcycle" | "bicycle";
  wheels: 2 | 4;
};

type IsTwoWheeler<T> = T extends { wheels: 2 } ? true : false;

const isBicycle: IsTwoWheeler<Vehicle> = true;

In this example, a complex conditional type IsTwoWheeler is used to determine if a given Vehicle type has two wheels.

9. Not Leveraging TypeScript Features

TypeScript offers various advanced features like union types, intersection types, and mapped types. Failing to leverage these features effectively can limit the expressiveness and power of your TypeScript code. Invest time in understanding and using these features to write more robust and concise code.

Some examples of TypeScript features that are not used effectively:

Leveraging Type Inference

const fruits = ["apple", "banana", "cherry"];
// TypeScript infers the type of 'fruits' as string[]

Here, TypeScript automatically infers the type of the fruits array as string[] based on the values it contains, eliminating the need for explicit type annotations.

Using Generic Types for Reusability

function identity<T>(arg: T): T {
  return arg;
}

const str: string = identity("Hello, TypeScript!"); // Type is inferred as string
const num: number = identity(42); // Type is inferred as number

In this example, we use a generic function identity that allows us to work with various types while maintaining type safety. This promotes code reusability and flexibility.

Type Aliases for Clarity

type Point = {
  x: number;
  y: number;
};

function calculateDistance(point1: Point, point2: Point): number {
  // Calculate distance
  return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
}

Here, we define a Point type alias to make the code more self-explanatory, maintainable, and readable.

10. Using third-party libraries

Using third-party libraries is common in modern development, but not all libraries have strong TypeScript support. Relying on poorly typed or untyped libraries can introduce type-related bugs into your codebase. Whenever possible, choose well-maintained libraries with TypeScript typings or write your own typings to ensure type safety.

Conclusion

TypeScript is a valuable tool for writing safer and more maintainable code, but it’s essential to use it correctly. By avoiding these common mistakes and following best practices, you can harness the full potential of TypeScript and build more reliable software.

Let me know if you need help by contacting me on Discord or write me an email at radu@genez.io.

Subscribe to our newsletter

Genezio is a serverless platform for building full-stack web and mobile applications in a scalable and cost-efficient way.



Related articles


More from Learning

Learning

Exploring genezio and Express.js

Stefan Iordache

May 05, 2023

Learning

What is Type Safety?

Kristopher Sandoval

Feb 21, 2024