DEV Community

Cover image for Why avoid using 'any' in TypeScript
Dany Paredes
Dany Paredes

Posted on • Originally published at danywalls.com

Why avoid using 'any' in TypeScript

When we create applications in Angular, the any type is our "live-saver" when struggle with complex error messages, want to save time by not writing specific type definitions, or think that TypeScript's type checking limits our coding flexibility and freedom.

Using the any type might seem like an easy solution to common problems, but it's important for developers to think about the hidden issues and the real effects of using this simple method.

Using the any type a lot can accidentally weaken TypeScript main purpose, which is to make code safer and find errors early. Ignoring the advantages of checking types can lead to hidden mistakes, harder-to-maintain code, and more complex code.

Today, I will to show few reasons with examples, why I avoid using any and instead embrace unknown. I will also discuss how to utilize TypeScript utility types to create flexible new types. Let's get started.

Type Safety

When you pick any type might seem like an easy solution for typing issues. However, this choice has a big drawback: we lose type safety, which is the main feature of TypeScript, to ensuring your code works correctly.

I want to show the dangers of not considering type safety with any, and highlight how important TypeScript's type system is.

Consider the following code snippet, we have the accountBalance method which expects an account and amount to update the balance.

export class Accounting {

  accountBalance(account: any, amount:any) {
    return account.balance += amount;
  }
}
Enter fullscreen mode Exit fullscreen mode

Since the accountBalance method expects an 'any' type for both account and amount, I can pass the following parameters:

let accounting = new Accounting()
let balanceA = accounting.accountBalance({accountNumber: '05654613', balance: 15}, 26)    
let balanceB = accounting.accountBalance({accountNumber: '05654613', balance: 15}, '26')
console.log({balanceA, balanceB})
Enter fullscreen mode Exit fullscreen mode

Is this the expected result? Why does everything compile and appear to work fine, but fail during runtime?

rerere

When working with TypeScript, we expect TypeScript, the IDE, or the compiler to warn us about these kinds of issues 🤦‍♂️.

Why doesn't the compiler warn about the issue? I believe the most effective solution is to introduce the Account type.

export type Account = {
  accountNumber: string;
  balance: number;
}
Enter fullscreen mode Exit fullscreen mode

Change the signature of the accountBalance function to use the Account type

 accountBalance(account: Account, amount: number): number {
    return (account.total += amount);
  }
Enter fullscreen mode Exit fullscreen mode

With appropriate typing, the IDE, compiler, and application all notify us of potential issues.

rerer

![rere(https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xggy9kjcs6ltrr4ch1y4.png)

rere

Perfect! Now we know how to use the function and avoid bugs during runtime. Let's take a look at another scenario.

The IDE WebStorm & VSCode

We love TypeScript because it offers more than just assistance with code; it enhances Visual Studio Code and WebStorm by providing powerful features such as auto-complete, code navigation, and refactoring, all of which rely on TypeScript's types.

But when we use any type safety and these useful tools. We'll see how using any too much can make coding worse, and how good typing makes your code safer for refactoring.

For example, we have a method called updateAccount that accepts an account of any type, as well as an empty object.

account: any = {}
  DEFAULT_BALANCE = 3000

  updateAccount(account: any) {
    account.accountID = Math.random().toString();
    account.balance = this.DEFAULT_BALANCE;
    return account;
  }
Enter fullscreen mode Exit fullscreen mode

Everything works fine, but what happens if I want to refactor 'accountNumber' to 'id' and 'balance' to 'total'?

I'm using the refactoring tools in WebStorm to modify the variable names, but when I change accountID to id and 'balance' to 'total', the IDE fails to make all the necessary adjustments.

f

We can resolve this issue by changing the type from any to Account. After making this change, we can confidently proceed with the refactoring.

updateAccount(account: Account): Account {
    account.id = Math.random().toString();
    account.total = this.DEFAULT_BALANCE;
    return account;
  }
Enter fullscreen mode Exit fullscreen mode

We can refactor once more, and the IDE will catch everything, ensuring a smooth process.

f

How about the account object? Instead of using 'any', let's change it to 'Account'. The IDE will notify us that the object is not initialized and also requires all 'Account' properties.

f

f

Afterward, the IDE and compiler compel us to initialize all required properties.

DEFAULT_BALANCE = 3000;
  account: Account = {
    id: "DEFAULT_ID",
    total : this.DEFAULT_BALANCE
  }
Enter fullscreen mode Exit fullscreen mode

As you can see, using 'any' might save time, but sometimes the price to pay is not worth it.

What I can do?

Maybe you ask, what can you do when you want to have flexibility? Then, when that happens, the unknown type is your best friend. But before introducing the unknown and utility types comes to help you.

Before to introduce unknown, let show the the differences between any and unknown.

  • any permits any operation, which can lead to runtime errors.

  • unknown require explicit type validation before its use.

Any Type

The any type lets you do anything without checking types, making TypeScript more flexible. This can be helpful, but also risky, as it may cause hard-to-find errors during program execution.

Example:

let riskyData: any = getDataFromAPI();
console.log(riskyData.name); // Compiles fine, even if riskyData doesn’t have a name property.
riskyData(); // Compiles fine, even if riskyData is not a function.
Enter fullscreen mode Exit fullscreen mode

In the example, TypeScript doesn't give any warnings about riskyData because it has the "any" type. This can cause errors if riskyData doesn't have the features we think it has.

Unknown Type

The unknown type is safer than any. It stands for any value but limits random actions on those values. To use an unknown value, you need to do specific checks to find out its type.

Example:

let safeData: unknown = getDataFromAPI();
console.log(safeData.name); // Error: Object is of type 'unknown'.
if (typeof safeData === 'string') {
   // we can perform because now TypeScript knows it’s a string.
    console.log(safeData.toUpperCase()); 
} else if (typeof safeData === 'object' && safeData !== null && 'name' in safeData) {
   // Safe, as we checked that 'name' is a property on safeData.
    console.log(safeData.name); 
}
Enter fullscreen mode Exit fullscreen mode

In this example, TypeScript makes sure you can only use the name property or change safeData to uppercase after checking its type. This helps avoid errors by making sure the actions on the data are safe.

I'm lazy to create types

I know maybe we want to create a type with just a few properties, or miss one from Account; then, for these situations, the TypeScript utility types can help you.

These utility types help you create types from others, but the ones I use most are Pick and Omit, which are utility types that help eliminate the need for any:

  1. Pick: Creates a type with selected properties from another type. Example: type AccountID = Pick<Account, 'id'>;

  2. Omit: Generates a type excluding specific properties. Example: type AccountWithoutID = Omit<Account, 'id'>;

Recap

I know using any can be tempting, but it should be approached with caution. Now that we understand the implications and the price to pay when using it, I believe that employing alternatives such as unknown and utility types will make your life easier and result in a better code base.

re

If you want to learn more about types feel free to checkout these other articles:

Top comments (0)