Skip to Content
Course content

69: Type Testing with tsd or expect-type

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're a precision machinist. You're carving a bolt out of titanium for a high-performance engine. It's not enough for the bolt to "look like a bolt" or even "fit roughly" into the hole. If it's off by 0.01 millimeters, the whole engine could seize at 7,000 RPM. You don't wait until the engine is running to find out if the bolt is the right size; you use a micrometer to measure the part in isolation. If the measurement is wrong, you scrap the part before it ever touches the engine.

In TypeScript, your types are the blueprints, and your logic is the engine. Usually, we rely on the compiler to tell us when something is wrong while we're writing code. But when you're building a library or a complex set of generic utilities, you need a "micrometer" for your types. You need a way to assert that a specific function always returns exactly string | number and never accidentally slips into string | number | undefined. That's where expect-type and tsd come in.

Why run tests on things that don't execute?

I've seen plenty of developers shrug and say, "If the code compiles, the types are correct." That's usually true for app code, but it's a dangerous assumption for utility libraries. If you change a internal type alias in a complex generic, the compiler might still be "happy" because the types are compatible, but you might have accidentally widened a type, losing the very type safety you promised your users.

Type testing allows you to write "assertions" that the TypeScript compiler checks. If the type of a variable doesn't match what you expected, the test fails during the build process—long before the code ever hits a browser or a server.

Putting your types under the microscope with expect-type

expect-type is the lightweight choice. It's essentially a set of helper functions that do nothing at runtime but trigger a compiler error if the types don't match perfectly. Let's say we have a utility that wraps a value in a standard API response envelope.

import { expectType, expectError } from 'expect-type';

interface ApiResponse<T> {
  data: T;
  status: number;
}

function wrapResponse<T>(data: T): ApiResponse<T> {
  return { data, status: 200 };
}

// This is our "micrometer" measurement. 
// If wrapResponse ever stops returning ApiResponse<string>, this line will throw a TS error.
expectType<ApiResponse<string>>(wrapResponse('Hello World'));

// We can also test that something SHOULD fail.
// This ensures that we aren't accidentally allowing numbers where strings should be.
expectError(wrapResponse<string>(123));

Notice how expectType doesn't actually "run" in the traditional sense of a Jest test. It creates a type-level constraint. If you're using a tool like tsd, it will scan these files and report any TypeScript errors as test failures.

Scaling up to tsd for library authors

While expect-type is great for quick checks, tsd is more of a full-fledged testing framework for types. It allows you to create .test-d.ts files. The magic of tsd is that it can check for "exact" type matches, which is crucial when you're dealing with complex unions or mapped types.

I typically reach for tsd when I'm publishing a package to NPM. It lets me document exactly how the types should behave for the end-user. For example, if I have a type that converts a User object into a UserDTO, I can write a test that proves the password field is definitely stripped out of the resulting type, ensuring no developer can accidentally leak sensitive data just because a type was loosened.

The workflow is simple: you write your type assertions in a .test-d.ts file, run the tsd command, and it uses the TypeScript compiler to verify that every assertion holds true. It's a bit of overhead, sure, but it's the only way to sleep soundly knowing your generics aren't lying to you.




📋 Practical Task

Validating the UserRole Extractor

You are building a permissions system. You have a utility type called ExtractRole that should take a complex User object and return only the role string. However, you need to ensure that it correctly handles cases where the role might be missing or nested.

Your Task:

  1. Create a type User that has an optional profile object, which in turn has a role (either 'admin', 'editor', or 'viewer').
  2. Implement a utility type ExtractRole<T> that extracts that role string.
  3. Using expect-type (assume it is installed), write three type assertions:
    • Assert that a user with an 'admin' role returns exactly 'admin'.
    • Assert that a user with no profile returns undefined.
    • Use expectError to assert that passing a plain string into your ExtractRole utility (instead of a User object) causes a type error.

Note: Focus on the type-level assertions. You don't need to write the runtime logic for the extraction, just the type definitions and the tests.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.