Testing Types in TypeScript

Posted by: Suprotim Agarwal , on 3/26/2024, in Category TypeScript
Views: 3582
Abstract: Mastering type testing in TypeScript is essential for developers working on large-scale projects, ensuring type definitions align perfectly with code functionality. This concise guide emphasizes the critical nature of precise type verification, utilizing TypeScript's type system and external tools like expect-type and dtslint for thorough testing. It addresses common pitfalls, such as confusing type assignability with equality, and introduces modern testing techniques to maintain code integrity. With a focus on preventing runtime errors and enhancing maintainability, this tutorial equips developers with the strategies needed to ensure their TypeScript applications are robust and reliable.

When working with TypeScript, it’s important to make sure your types are correct. Just as you wouldn’t code without testing its functionality, you shouldn’t use types without verifying them.

This tutorial delves into the intricacies of testing types in TypeScript, providing insights into effective strategies, tools, and the latest advancements.

The Importance of Type Testing

Imagine you’re developing a TypeScript library or adding type declarations to an existing JavaScript library for a large-scale e-commerce platform.

How can you be confident that your types are correct?

Testing types might seem straightforward, but it’s riddled with nuances and potential pitfalls.

Just as you wouldn’t deploy code without tests, you shouldn’t overlook testing your type declarations. With TypeScript’s growing adoption, ensuring that your types are accurate is crucial for maintainability and preventing runtime errors.

Methods of Type Testing

In TypeScript, there are primarily two methods to test types:

1. Using the TypeScript Type System: This involves leveraging TypeScript’s built-in type system to validate types. For instance, using helper functions like assertType or directly assigning types to variables to see if they match the expected type.

2. Employing External Tooling: This method involves using external libraries and tools, such as expect-type, dtslint, and eslint-plugin-expect-type, to validate and test types. These tools provide specialized functionalities that can make type testing more comprehensive and intuitive.

With these methods in mind, let’s delve deeper into their nuances.

Common Mistakes in Type Testing

Consider a scenario where you’ve defined a type for a filter function, similar to the ones found in utility libraries like Lodash:

declare function filter<T>(array: T[], predicate: (item: T) => boolean): T[];

A naive way to test this might be to simply call the function:

filter([10, 20, 30, 40], num => num > 25);

While this does some basic error checking, it doesn’t truly validate the type’s behavior. It’s akin to running a function to see if it throws an error without checking its output.

A slightly better approach might involve assigning the result to a variable with a specific type:

const largeNumbers: number[] = filter(['Suprotim', 'Mahesh'], name => name.startsWith('M'));

However, this method has its drawbacks. For one, you’re introducing potentially unused variables, leading to linting issues. A common workaround is to use a helper function:

function assertType<T>(x: T) {}
assertType<number[]>(filter([5, 15, 25, 35], num => num < 20));

But even this approach isn’t foolproof. It checks for type assignability rather than type equality, which can lead to subtle issues.

Delving Deeper: Assignability vs. Equality

TypeScript’s type system is structural, meaning it’s based on the shape of data rather than its nominal type. This can lead to surprising results when testing types.

For instance, consider the following:

const musicians = ['Sonu', 'Arijit', 'KK'];
assertType<{name: string}[]>(
  filter(musicians, name => name.startsWith('K'))
);

The above code doesn’t raise any type errors, even though the actual type returned by the filter function is just an array of strings. This is because an array of strings is assignable to an array of objects with a name property of type string, but they aren’t equal.

Traditional Approaches and Their Limitations

As previously discussed, there are two primary ways to test types: using the type system and employing external tooling. While these methods have their merits, they also come with certain pitfalls.

For instance, consider the filter function example:

declare function filter<T>(array: T[], predicate: (item: T) => boolean): T[];

While testing such declarations, merely checking for errors isn’t sufficient. It’s essential to validate the return types and ensure they align with expectations.

Tools to the Rescue

Given the complexities of type testing, it’s advisable to leverage tools designed for this purpose. Here are some popular options:

1. expect-type

A library that operates within the TypeScript type system, expect-type offers a more intuitive way to test types:

import { expectTypeOf } from 'expect-type';
const bands =['Nirvana', 'Parikrama', 'DotNetCurry'];
expectTypeOf(filter(
  bands,
  name => name.startsWith('N')
)).toEqualTypeOf<string[]>();

2. dtslint

Designed for testing type declarations in the DefinitelyTyped repository, dtslint uses specially formatted comments:

const artists = ['Sonu', 'Arijit', 'KK'];
filter(artists, name => 
  name.startsWith('B')
);  // $ExpectType string[]

3. eslint-plugin-expect-type

An eslint plugin that offers a similar approach to dtslint but is more suited for testing types in your own projects.

Modern Testing Techniques: Mocha/Chai with TypeScript

One of the significant advancements is the enhanced integration of Mocha/Chai with TypeScript. This combination provides a robust framework for writing and executing tests, especially for types.

For instance, the previous method of setting up Mocha/Chai in TypeScript required specific packages:

However, with the latest update, it’s recommended to include cross-env:

npm install chai mocha ts-node cross-env @types/chai @types/mocha --save-dev

This addition ensures that the test environment variables are set correctly across all platforms. The test command has also been modified to use cross-env:

"test": "cross-env TS_NODE_PROJECT='./tsconfig.test.json' mocha"

Coverage: A Critical Aspect

Another pivotal aspect of testing is determining how much of your code is covered by tests.

Code coverage is a metric that helps you understand the percentage of your codebase tested by your test suite. It’s a crucial aspect of ensuring the reliability of your application.

The nyc package has gained traction for this purpose. It seamlessly integrates with Mocha and Chai, providing detailed coverage reports.

To use nyc, simply prefix it before mocha in your test command:

"test": "cross-env TS_NODE_PROJECT='./tsconfig.test.json' nyc mocha"

Generate the coverage report by executing the following command:

$ npm run test:coverage

After running your tests with nyc, it produces a summary in the console and a detailed report in the /coverage directory.

Code Coverage

If you check the coverage folder in your project, you can access the HTML report for the same.

Code Coverage Report

This report indicates that the index.ts file has 100% statement coverage, 100% branch coverage, 100% function coverage, and 100% line coverage. The uncovered line numbers (none in our case) are also displayed, guiding you to areas that might need more tests.

Caveats and Considerations

While tools like expect-type and dtslint are powerful, they aren’t without limitations. For instance, they might be sensitive to type order (1|2 vs. 2|1) or unable to detect nuances in type displays.

Moreover, some type behaviors, like autocomplete suggestions for union types, can’t be tested using the methods described above.

Example of Testing Classes in TypeScript

When working with classes in TypeScript, it’s essential to ensure that the methods and properties behave as expected. Let’s consider a simple User class:

class User {
  constructor(public name: string, public age: number) {}

  greet() {
    return `Hello, ${this.name}!`;
  }
}

To test this class, you can create an instance and check its properties and methods:

const user = new User('Suprotim', 40);
assertType<string>(user.name);
assertType<number>(user.age);
assertType<string>(user.greet());

Key Learnings

  • Understand the difference between type assignability and equality.
  • Utilize tools like expect-type, dtslint, and eslint-plugin-expect-type for type testing.
  • Be cautious of the limitations and nuances in TypeScript’s type system.
  • Always prioritize thorough testing to ensure type accuracy and safety.
  • Utilize modern tools like Mocha/Chai and nyc for a comprehensive testing experience.
  • Stay updated with the latest advancements in TypeScript testing methodologies.

By following these guidelines and leveraging the right tools, you can ensure that your TypeScript types are robust, accurate, and reliable.

Conclusion

Testing types in TypeScript is an intricate process, but with the right tools and methodologies, it becomes a manageable task.

As we move forward, the TypeScript community’s collective knowledge and experience will undoubtedly lead to even more efficient and reliable testing techniques.

This article has been technically reviewed by Mahesh Sabnis.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of DotNetCurry, DNC Magazine for Developers, SQLServerCurry and DevCurry. He has also authored a couple of books 51 Recipes using jQuery with ASP.NET Controls and The Absolutely Awesome jQuery CookBook.

Suprotim has received the prestigious Microsoft MVP award for Sixteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.

Get in touch with him on Twitter @suprotimagarwal or at LinkedIn



Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!