Implicitly Typed ‘any[]’ in TypeScript: Unraveling Potential Pitfalls and Best Practices

Category: Programming, TypeScript | July 25, 2023

Introduction

TypeScript is a superset of JavaScript that also possesses implicit typing feature. In Typescript, implicit typing means that when you declare a variable without specifying its type, Typescript will interpret the type based on the initial value of the variable. For instance:


let greeting = "Good Morning!"

In this example, Typescript can easily infer that `greeting` is of type string.

However, there’re some cases where Typescript can’t infer the type. As a result, it will automatically assigns the type as any. This essentially implies that the variable can hold a value of any type. Let’s look an example:


let greeting; // This will implicitly have an 'any' type

For an array, the any[ ] type means an array can hold elements of ‘any’ type. Here’s an example:


let array = ["Hello World", 4, false, undefined, null];

In this case, the `array` variable can store multiple values of different data types because it’s implicitly of type ‘any[]’.

The ‘any’ type in TypeScript is a special type that essentially disables type checking for the variable or expression it is assigned to. It allows variables to hold values of any type without type enforcement. Since ‘any’ type is lack of type safety, using ‘any’ extensively in code can undermine the benefits of using TypeScript, as type errors won’t be caught during development, and the code might become less predictable and harder to maintain.

Understanding Implicit Typing in TypeScript

Recap of explicit typing and type inference in TypeScript

Explicit typing is when you explicitly define the type of a variable at the time of declaration. Typescript will ensure that any future value assigned to that variable matches the defined type. For example:


let greeting: string = "Good Morning!";
greeting = 123 // This will result in a Typescript error as `123` doesn't match 'string' type

On the other hand, Typescript also supports type inference to automatically infers the type based on the value assigned to a variable or the expression’s return value when you don’t explicitly type. Here’s the example:


let greeting = "Good Morning!"
greeting = 123 // This will result in a Typescript error

In this case, the compiler automatically infers type of `greeting` variable as string type because it’s not explicilty typed. As a result, it’ll throw Typescript error if it’s assigned value `123`, which has a type of number.

Explanation of implicit typing and its role in type inference

Again, implicit typing is the opposite of explicit typing (need to explicitly declare the type of the variables). Implicit typing is a feature where Typescript infers the type of a variable if it’s not explicitly typed, based on the initial value assigned to it. For instance:


let name = "Cristine!"
name = [1, 2, 3] // Compile Error: Type 'number[]' is not assignable to type 'string'

Implicit typing plays a crucial role in providing code flexibility and reduced code verbosity while still providing the benefits of static typing. This is due to the fact that it omits explicit type annotations for the targeted variables and expressions.

Note: Static typing means that the variables must have a value that matches the explicit / implicit type assigned to it during compilation.

The dangers of relying on implicit typing

While implicit typing in Typescript can provide flexibility, it can lead to potential dangers and pitfalls if used inappropriately. The most significant issue may arise when Typescript implicitly infers the ‘any’ type. This typically happens when a variable is declared without a value:


let var; // This implicitly has the 'any' type.
var = "Hello World"; // Value assignment is appropriate.
var = 246; // This is also fine, which could be problematic!

The variable, `var` can now take any type of value, which may lead to unforeseen behavior and bugs that are hard to track. It essentially disables Typescript’s main advantage – static typing.

The Pitfalls of Implicitly Typed ‘any[]’

Lack of Type Safety and Potential Runtime Errors

Using implicitly typed ‘any[ ]’ in Typescript can lead to runtime errors due to lack of type safety because the compiler won’t catch type-related issues during development. Here’s a simple example:


let array = [2, "three", true]; // `array` is implicitly of type 'any[ ]'
let firstElement =array[0];
console.log(firstElement.toUpperCase()); // This will result in runtime error if `firstElement` is not a string

Impact on Code Maintainability and Readability

Implicitly typed ‘any[]’ can negatively impact the maintainability and readability of the code. Without explicit type annotations, it becomes more difficult for other developers (and even for yourself in the future) to understand what types of data are expected in the array. As a project grows larger, the lack of clear type information can lead to confusion and hinder the ability to reason about the codebase effectively. Here’s the example:


const items = [1, "apple", { name: "banana" }]; // `items` is implicitly of type 'any[ ]'

function process(items) {
  // Problem 1: What are the expected types of 'items'? It's unclear without explicit typing.
  items.forEach((item) => {
    // Problem 2: What type does 'item' have here? It's hard to know without explicit typing, which can lead to potential bugs.
  });
}

In this case, `items` is implicitly typed ‘any[]’, and the lack of explicit type information in the `process` function makes it challenging to understand the expected data types. This can lead to difficulties when maintaining and extending the codebase over time.

Difficulty in Identifying Bugs and Enforcing Strict Typing

Using implicitly typed ‘any[]’ can make it harder to identify bugs and enforce strict typing within the codebase. Since TypeScript does not provide type checking for ‘any’ type, you lose the benefits of static typing and cannot rely on the compiler to catch type-related issues.Consider the following example:


let items = [1, "banana", "orange"]; // `items` has implicit type of 'any[ ]'

// Compiler warnings and errors are suppressed
const item: string = products[0]; // No compile-time error, but will throw an runtime error if 'products[0]' contains non-string

Common Scenarios of Implicit ‘any[]’

Looping through arrays without type annotations

One common scenario where implicitly typed ‘any[]’ may arise is when you loop through arrays without specifying type annotations for the variables. This situation typically occurs when you use the ‘for…of’ loop or the ‘forEach’ method on an array:


const fruits = ["apple", "banana", "orange"]; // `fruits` has implicit type of 'any[ ]'

// 1. `for...of`
for (const fruit of fruits) {
  // 'fruit' is implicitly typed as 'any'
  console.log(fruit.toUpperCase()); // No compile-time error, but might throw runtime error if 'fruits' contains non-strings.
}

// 2. `forEach`
fruits.forEach((fruit) => {
  // 'fruit' is implicitly typed as 'any'
  console.log(fruit.toUpperCase()); // No compile-time error, but might fail at runtime if 'fruits' contains non-strings.
});

`fruit` is implicitly typed as ‘any’ in both ‘for…of’ and ‘forEach’ loops. As a result, TypeScript won’t provide any compile-time type checking for the `fruit` variable, which may potentially cause runtime errors if the array contains elements that are not strings.

Handling return types of certain functions

Functions that return an array without explicit type annotations can result in implicitly typed ‘any[]’. This is common when working with APIs or third-party libraries that may not have well-defined types or when dealing with functions that return dynamically-typed arrays:


// `fetch()` function returns an array without explicit type annotation
function fetch(): any[] { return [1, "Good Morning!", true]; }

const data = fetch(); // Implicitly typed 'any[]'

data.forEach((item) => {
  // 'item' is implicitly typed as 'any'
  console.log(item.toUpperCase()); // No compile-time error, but might fail at runtime if 'data' contains non-strings.
});

In this example, the function `fetch()` returns an array with an implicitly typed ‘any[]’. The lack of type information for the elements in the array can lead to potential runtime errors if the array contains elements of unexpected types.

Dealing with loosely typed third-party libraries

While working with third-party libraries that are not written in TypeScript or lack explicit type declarations, it is common to encounter implicitly typed ‘any[]’. These libraries may use ‘any’ or provide no type information, causing TypeScript to infer arrays as ‘any[]’ when interacting with the library’s functions or data. Let’s look an example:


import thirdPartyLibrary from 'thirdPartyLibrary' // A third-party library without type declarations

const result = thirdPartyLibrary.getData(); // Implicitly typed 'any[]'

result.forEach((item) => {
  // 'item' is implicitly typed as 'any'
  console.log(item.toUpperCase()); // No compile-time error, but might fail at runtime if 'result' contains non-strings.
});

In this example, ‘thirdPartyLibrary.getData()’ returns an array without type annotations, causing `result` to be implicitly typed ‘any[]’. It may lead to potential runtime errors if the third-party library returns elements of unexpected types.

Best Practices for Avoiding Implicit ‘any[]’

Emphasize explicit type annotations

To avoid implicitly typed ‘any[]’, one of the best practices is to prioritize explicit type annotations. By explicitly defining the types for variables, function parameters, and return values, you can leverage the full benefits of TypeScript’s static typing:


const numbers: number[] = [1, 2, 3, 4]; // `numbers` has explicit type of 'number[ ]'

// Explicitly typed function with parameters and return type
function add(a: number, b: number): number {
  return a + b;
}

In this example, explicit typing provides clear type information that helps the compiler catch type-related errors.

Leveraging generics to enhance type safety

Generics allow you to create reusable components and functions that work with different types while preserving type safety. By using generics, you can avoid using ‘any[]’ and maintain more specific type information in your code. Here’s a simple example:


// Generic function with array parameter and return type
function reverseArray(arr: T[]): T[] {
  return arr.reverse();
}

const numbers: number[] = [1, 2, 3, 4];
const strings: string[] = ["apple", "banana", "orange"];

const reversedNumbers = reverseArray(numbers); // 'reversedNumbers' is inferred as 'number[]'
const reversedStrings = reverseArray(strings); // 'reversedStrings' is inferred as 'string[]'

In this example, the `reverseArray(arr: T[ ])` function is defined with a generic type parameter ‘T’. As a result, we can enforce a specific type such as string or number to make sure TypeScript can infer the type of the output array correctly, without resorting to ‘any[]’.

Utilizing TypeScript utility types

TypeScript provides utility types that can help improve type safety and reduce the need for ‘any[]’. Some useful utility types include ‘Partial’, ‘Pick’, ‘Record’, ‘Omit’, and ‘Exclude’. These utility types allow you to manipulate and compose types in a safer way. Let’s consider an example:


// Original type
interface User {
  id: number;
  name: string;
  age: number;
}

// Using 'Pick' utility type to create a new type with selected properties
type UserSummary = Pick;

const user: UserSummary = {
  id: 1,
  name: "John",
};

console.log(user.age); // Compile-time error, 'age' property is not part of 'UserSummary' ('age' shouldn't be defined)

In this example, the ‘Pick’ utility type helps us create a new type `UserSummary` that only includes the `id` and `name` properties from the `User` interface. This allows us to create a more specific type and avoid the use of ‘any[]’.

Properly documenting types and interfaces

Another important practice to avoid implicitly typed ‘any[]’ is to properly document your types and interfaces. Use descriptive names for types and provide detailed comments explaining the expected data structure.


/**
 * Represents a user object with essential information.
 */
interface User {
  id: number;
  name: string;
}

/**
 * Function to fetch a list of users.
 * @returns An array of User objects.
 */
async function fetchUsers(): Promise {
  // Implementation...
}

async function displayUserData() {
  const usersData = await fetchUsers();

  usersData.forEach((user) => {
    console.log(`User ID: ${user.id}, Name: ${user.name}`);
  });
}

From the above example, the `User` interface and the `fetchUsers()` function are well-explained and properly documented, providing clear information about the expected structure of the data and its usage.

Advanced TypeScript Configuration

Enabling strict mode and its advantages

Enabling strict mode in TypeScript means enabling a set of strict type checking options such as `noImplicitAny``strictNullChecks``strictFunctionTypes`, and so on to help catch more potential errors during development. The advantages of enabling strict mode include:

  1. Better Type Inference: Strict mode allows TypeScript infer more accurate types, reducing the need for explicit type annotations and providing better type checking.
  2. Preventing Implicit ‘any’: Strict mode prevents implicitly typed ‘any[]’ and other cases of ‘any’ usage, encouraging explicit typing.
  3. Type Checking for Function Return Types: Strict mode enforces type checking for function return types, ensuring that functions return the correct types as specified.

To enable the strict mode, you need to set the `strict` compiler option to `true` in your `tsconfig.json` file:


{
  "compilerOptions": {
    "strict": true
  }
}

Customizing the type checking process

TypeScript allows you to customize the type checking process by configuring various compiler options if you don’t want to use “strict”: true. Here’re some type checking options:

  1. `noImplicitAny`: Prevents variables and parameters from being implicitly typed as ‘any’
  2. `strictNullChecks`: Enforces strict null checks, which help catch potential null and undefined errors by narrowing down the types
  3. `noUnusedLocals` and `noUnusedParameters`: Detects and reports unused variables and parameters, allowing cleaner and more maintainable code.

Here’s a simple example:


{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false
  }
}

Recommended compiler options for safer code

For safer code, it is recommended to either enable the strict mode or customize severals compiler checking options, depending on your project’s requirements to enhance the safety and maintainability of your TypeScript codebase.

Migrating Legacy Code and Dealing with Third-party Libraries

Strategies for gradually introducing explicit typing

Migrating legacy code to TypeScript can be a gradual process. Start by enabling TypeScript in your project and applying explicit typing where it makes sense. You can adopt these strategies:

  1. Type Annotations for Variables: Add explicit type annotations to variables where the type is clear but not explicitly stated.
  2. Function Signatures: Specify types for function parameters and return values to catch type-related errors and improve code understanding.
  3. Interfaces and Type Aliases: Use interfaces and type aliases to define complex types and promote reusable code.
  4. Gradual Refactoring: Don’t try to type everything at once. Refactor a section of code, apply strict typing, and continue in iterations.

Simple example of explicit typing:


// Before adding explicit typing
function calculateTotal(price, quantity) {
  return price * quantity;
}

// After adding explicit typing
function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

Wrapping third-party libraries with custom typings

Third-party libraries may lack TypeScript type declarations or have outdated ones. To improve type safety and leverage TypeScript’s capabilities, you can create custom typings or use existing community-driven typings.

Let’s create a custom typing for the ‘lodash’ library to make TypeScript aware of its functions and types:

1. Create a file named `lodash.d.ts` in your project:


// lodash.d.ts
declare module 'lodash' {
  export function chunk(array: T[], size: number): T[][];
  // Add more typings for other lodash functions as needed.
}
}

2. In your `tsconfig.json`, ensure the ‘types’ property includes the custom typings:


{
  "compilerOptions": {
    "types": ["lodash"]
  }
}

By this way, the compiler understand the types and APIs of third-party JavaScript libraries or custom typings you may have created./p>

When to consider using ‘any’ and mitigating its risks

While TypeScript promotes strict typing, there are cases where using ‘any’ is necessary or beneficial. However, it’s essential to use ‘any’ appropriately and mitigate its risks:

  1. Migration Process: During the migration of legacy code, ‘any’ can be a temporary solution when the type is uncertain.
  2. Integration with Untyped Libraries: When working with third-party libraries that lack type declarations, ‘any’ can be used to interface with them. However, try to wrap these libraries with custom typings for better type safety.
  3. Dynamic Data or Unsafe Operations: In some cases involving dynamic data (e.g., parsing JSON from an external source such as APIs / server), using ‘any’ may be ideal as you don’t have full control over the data structure.

Here’s a simple example of using ‘any’ for API call:


// Fetching data from the API (using a simplified fetch function)
async function getData(): Promise {
  const response = await http.get('https://example.com/api/data');
  const data = await response.json();
  return data;
}

In this example, if we’re not sure about the types or data structures of the returned response, we can ideally use ‘any’ type to avoid any runtime error.

To mitigate the risks of using ‘any’, consider these practices:

  1. Limit ‘any’ Usage: Try to minimize the use of ‘any’ to specific scenarios where it is truly necessary.
  2. Use Type Assertions Judiciously: Use type assertions (‘as’ syntax) when you’re sure about the type of data being used with ‘any’.
  3. Gradual Refactoring: Aim to refactor ‘any’ usages incrementally, replacing them with specific types as you gain better understanding or control over the data.

Real-world Examples and Case Studies

Implicit ‘any[]’ can lead to various consequences, including lack of type safety, potential runtime errors, and reduced code maintainability. Let’s explore a real-world example to demonstrate these consequences:

Consider a simple shopping cart application where users can add products to their cart and calculate the total price:


// shoppingCart.ts
let cartItems = []; // `cartItems` has implicit type of 'any[ ]'

function addToCart(item) {
  cartItems.push(item);
}

function calculateTotal() {
  let total = 0;
  for (let item of cartItems) {
    total += item.price; //! Potential runtime error if 'item' doesn't have 'price' property
  }
  return total;
}

const product1 = { name: 'Shirt', price: 20 };
const product2 = { name: 'Shoes', price: 50 };

addToCart(product1);
addToCart(product2);

const totalPrice = calculateTotal();
console.log('Total Price:', totalPrice);

In this example, `cartItems` is implicitly typed ‘any[]’ to store the added products. The `addToCart()` function allows users to add any value, regardless of its type to the `cartItems`, and the `calculateTotal()` function calculates the total price of the items in the `cartItems`.

This will undoubtedly lead to several pitfalls:

  1. Lack of Type Safety: The use of implicitly typed ‘any[]’ for `cartItems` means that TypeScript does not perform any type checking on the elements added to the array. It allows any type of data to be added to the cart, which could lead to unexpected behavior or runtime errors.
  2. Potential Runtime Errors: In the `calculateTotal()` function, when calculating the total price, (total += item.price), TypeScript does not know the structure of `item`, and if it doesn’t have the `price` property, it will result in a runtime error.
  3. Reduced Code Maintainability: The lack of explicit type annotations makes it harder for other developers to understand the expected types of data in the cart. As the application grows, maintaining and extending code with implicitly typed arrays becomes challenging.

Now, let’s refactor the codes to improve type safety and address the issues casued by implicit type ‘any[ ]’:


// shoppingCart.ts
//* Specify the interface of the 'Product'
interface Product {
  name: string;
  price: number;
}

let cartItems: Product[] = []; //* `cartItems` has explicit type of 'Product[]'

//* 'addToCart' function only accept 'item' of type 'Product'
function addToCart(item: Product) {
  cartItems.push(item);
}

function calculateTotal(): number {
  let total = 0;
  for (let item of cartItems) {
    total += item.price; //* TypeScript infers 'item' as type 'Product', no runtime errors
  }
  return total;
}

const product1: Product = { name: 'Shirt', price: 20 };
const product2: Product = { name: 'Shoes', price: 50 };

addToCart(product1);
addToCart(product2);

const totalPrice = calculateTotal();
console.log('Total Price:', totalPrice);

We first define an interface `Product`, defining the structure of a product with `name` (string) and `price` (number) properties. Then, we explicitly type the `cartItems` array as `Product[]`, ensuring it can only contain elements of the `Product` type. Thirdly, we add type annotations to the `addToCart()` function parameter, specifying that it should be of type `Product`. Finally, in `calculateTotal()` function, TypeScript now knows that `item` is of type `Product`, and it can safely access the `price` property without any runtime errors.

In this way, we did improve type safety, code maintainability and prevent compile-time error throughout our codebase.

Tools and Resources for Improving TypeScript Development

TypeScript has a vibrant ecosystem with various tools, resources, and community support to enhance your development experience. Let’s explore some popular TypeScript linting tools, online resources, community support platforms, and recommended readings and tutorials.

Linting Tool:

  1. ESLint with TypeScript Plugin: ESLint is a widely used linting tool for JavaScript, and you can use it with the TypeScript plugin (`@typescript-eslint/eslint-plugin`) to lint TypeScript files.

Online Resources, Community Support, Readings:

  1. TypeScript Official Website: The official TypeScript website, https://www.typescriptlang.org/ provides documentation, guides, and the latest updates about TypeScript.
  2. Stackoverflow Website: The TypeScript tag on Stack Overflow, https://stackoverflow.com/questions/tagged/typescript is a great place to ask questions and find answers related to TypeScript development.

Conclusion

Let’s recap again the findings of this article. Avoiding implicit ‘any[]’ and embracing explicit typing are essential practices to improve type safety in your codebase. By leveraging TypeScript’s features and adhering to best practices, developers can write more reliable, maintainable, and scalable applications. As TypeScript continues to evolve, it will likely become an even more robust and indispensable tool for JavaScript developers, ensuring better type safety and enhancing the overall development experience. Thank you and have a nice day!:)