Photo by Robert Zunikoff on Unsplash
Why avoid using 'any' in TypeScript
Reasons for Favoring 'unknown' over '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's 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;
}
}
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})
Is this the expected result? Why does everything compile and appear to work fine, but fail during runtime?
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;
}
Change the signature of the accountBalance
function to use the Account
type
accountBalance(account: Account, amount: number): number {
return (account.total += amount);
}
With appropriate typing, the IDE, compiler, and application all notify us of potential issues.
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;
}
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.
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;
}
We can refactor once more, and the IDE will catch everything, ensuring a smooth process.
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.
Afterward, the IDE and compiler compel us to initialize all required properties.
DEFAULT_BALANCE = 3000;
account: Account = {
id: "DEFAULT_ID",
total : this.DEFAULT_BALANCE
}
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.
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);
}
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:
Pick: Creates a type with selected properties from another type. Example:
type AccountID = Pick<Account, 'id'>;
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.
If you want to learn more about types feel free to checkout these other articles: