· hands on

Testing Types in TypeScript

Testing types in TypeScript is crucial for SDK and API developers to ensure predictable type inferences. Tools like `tsc`, `dtslint`, `tsd`, and `Vitest` can help catch errors early and guarantee type behavior.

If you’re building SDKs or APIs for third-party developers, it makes sense to write tests for your types. Especially if you want to guarantee that consumers get awesome, predictable type inferences, not frustrating surprises. In this article, we’ll explore how to test your types, from quick compiler tricks to dedicated tooling.

Contents

Why test types?

Normally, TypeScript types vanish at runtime, so you can’t “run” them like a function. But that doesn’t stop you from verifying them during development, catching wrong assumptions early in your type definitions or APIs.

For library authors, this is huge: you can lock in guarantees about how your types behave, even across future refactors.

Testing types with tsc

The most lightweight way is just using tsc with @ts-expect-error:

interface User {
  id: number;
  name: string;
}
 
const good: User = { id: 42, name: 'Alice' };
 
// This directive tells TypeScript that you expect this line to fail.
// @ts-expect-error
const bad: User = { id: 'nope', name: 'Alice' };

If you later change the type of id to string, the bad assignment will become valid, and the @ts-expect-error will itself raise an error.

Testing types with dtslint

dtslint was the first dedicated tool for type testing, created by members of the TypeScript team such as Nathan Shively-Sanders, Orta Therox, and Andrew Branch. It works with special comment directives:

// $ExpectType string
String(42);
 
// $ExpectError
String(true).toFixed();

Although it’s deprecated today and has largely been replaced by newer approaches like tsd, it’s still in use within the DefinitelyTyped repository and can be handy for legacy projects.

Testing types with tsd

The tsd project is backed by well-known developers such as Sindre Sorhus, Matteo Collina, and Orta Therox. With tsd, you can write tests for your type definitions (your .d.ts files) by creating files with the .test-d.ts extension. This convention may look unusual at first and requires you to generate .d.ts files from your code beforehand.

Consider the following code:

index.ts
export function concat(a: string | number, b: string | number) {
  return `${a}${b}`;
}

When compiled with the declaration flag enabled, it produces this .d.ts file:

index.d.ts
export declare function concat(a: string | number, b: string | number): string;

Based on this declaration, we can create a index.test-d.ts file to check our types:

index.test-d.ts
import { expectType } from 'tsd';
import { concat } from './index.js';
 
expectType<string>(concat('Ben', 'ny'));
expectType<string>(concat(7, 2));
// @ts-expect-error
expectType<number>(concat(7, 2));

When running tsd on this code, we will receive a successful test result.

Got it. Here’s a revised Vitest section that emphasizes it being the most natural approach:

Testing types with Vitest

If you’re already using Vitest for unit tests, the most natural way to test your types is by using the expectTypeOf helper. It follows the same style and structure as regular unit tests, so you don’t need to learn a new mental model.

concat.test.ts
import { describe, it, expectTypeOf } from 'vitest';
import { concat } from './index.js';
 
describe('concat', () => {
  it('returns a string', () => {
    expectTypeOf(concat('Ben', 'ny')).toEqualTypeOf<string>();
    expectTypeOf(concat(7, 2)).toEqualTypeOf<string>();
  });
});

Unlike tsd, this approach doesn’t require generating .d.ts files or maintaining separate .test-d.ts files. You simply write your type tests next to your runtime tests. Remember to update your test command to vitest --typecheck so type testing is enabled.

Back to Blog