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.

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

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.
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!
Was this article worth reading? Share it with fellow developers too. Thanks!
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