A Deep Dive into Typescript Type Testing

A Deep Dive into Typescript Type Testing

One critical aspect of library development is testing TypeScript types.We will explore three approaches to type testing: Vitest, tsd and custom solutions. By the end of this post, you will have a better understanding of these techniques and their applications.

Vitest is a test runner based on Vite, which is widely used for testing runtime code. In addition to its popularity for runtime testing, Vitest also provides APIs to test your TypeScript types.

To get started with Vitest, you can use the following example:

import { expectTypeOf } from "vitest";
import { calculateArea } from "./geometry";

test("calculateArea type test", () => {
  expectTypeOf(calculateArea).toBeFunction();
  expectTypeOf(calculateArea)
    .parameter(0)
    .toMatchTypeOf<number>();
  expectTypeOf(calculateArea).returns.toMatchTypeOf<number>();
});

By using Vitest, you can colocate your type tests with your runtime tests, providing a more cohesive testing experience.

2. tsd: A Dedicated Library for Type Testing

tsd is a dedicated library for testing TypeScript types. It provides a simple API to test your types and is easy to integrate into your existing test suite.

To get started with tsd, you can use the following example:

First, install tsd as a development dependency:

npm install --save-dev tsd

Then, create a test file:

// test-d/index.test-d.ts

import { expectType } from "tsd";
import { identityFunc } from "../src/identityFunc";

expectType<string>(identityFunc("hello"));
expectType<number>(identityFunc(42));

To run your tsd tests, execute the following command:

npx tsd

3. Custom Solutions: A Flexible Approach

If you're not using Vitest or prefer a custom solution, you can create your own type testing approach using TypeScript's type helpers, such as Expect and Equal. These helpers allow you to be specific with the types you're narrowing down.

To get started with custom solutions, you can use the following example:

type Expect<T extends true> = T;
type Equal<X, Y> = Expect<X extends Y ? (Y extends X ? true : false) : false>;

const makePair = <T, U>(first: T, second: U): { first: T; second: U } => {
  return { first, second };
};

it("Should return an object with the passed parameters as properties", () => {
  const testPair = makePair("name", 42);

  type test = Expect<Equal<typeof testPair, { first: string; second: number }>>;
});

When running your custom test suite, you can use the TypeScript CLI (tsc) to check if any type tests fail.

In some cases, you might want to ensure that specific errors are thrown in certain situations. To do this, you can use the ts-expect-error comment, which will fail if no error is detected on the following line. However, it's essential to note that this method only checks for the presence of an error, not a specific error.

Here's an example of using ts-expect-error:

const processUserData = <T extends { name: string; age: number }>(arg: T) => {};

it("Should not accept an object without name and age properties", () => {
  // @ts-expect-error
  processUserData({ name: "John" });
});

Summary

It's important to note that, in general, application development doesn't require extensive type testing. However, if you're working on a library that will be consumed by others, it's crucial to test your types thoroughly to provide a robust and reliable API.

In conclusion, type testing is an essential aspect of library development in TypeScript. The three approaches we discussed – Vitest, custom solutions, and tsd – offer various degrees of flexibility and ease of use. Understanding and mastering type testing will help you deliver high-quality TypeScript code that benefits a wide range of users. Happy coding!