Types have their own regressions that an ordinary test does not see. A function may return a correct value at runtime, but its signature has quietly drifted: a parameter widened, a field disappeared from the inferred type, a generic stopped narrowing. Vitest can check this too - it has type testing that is executed by the compiler rather than the engine, and catches contract breakages at the type level before a user of the API notices them.
The central tool is expectTypeOf. It does nothing at runtime: it is an assertion checked by tsc. expectTypeOf(fn).toEqualTypeOf<T>() requires an exact type match, while toExtend<T>() requires the type to be assignable to the target, that is, to be its subtype. An important version detail: the former toMatchTypeOf is deprecated, replaced now by toExtend, and for comparison by object shape there is toMatchObjectType.
Beyond equality, expectTypeOf can take a function apart. .parameter(0) extracts the type of the first argument, .returns the result type, .toBeCallableWith(...) checks that the function can be called with a given set of arguments. This lets you assert the contract pointwise: not the whole type at once but exactly the facet whose regression you want to catch - for example, that the first parameter stayed a string rather than widening to string | number.
Alongside stands assertType - a simpler assertion: assertType<T>(value) checks that an expression has the expected type and fails compilation if not. It is handy on a call's result, when it is enough to confirm the output type is exactly that and there is no need to take the signature apart. Both tools work at compile time: if the type does not match, the test will not run - tsc rejects it.
The key feature is how this executes. Type checks are not run by an ordinary pass: they are enabled by the vitest --typecheck flag. Under it Vitest runs tsc over the type-test files, and what goes red is not a failed assert but a compiler error. Files for such tests are usually named with a .test-d.ts suffix, to separate them from runtime tests and set a dedicated include on them. The mechanism itself is marked experimental in the documentation, just like benchmarks: it works, but its options may change between versions.
From this follows the main pitfall. Without --typecheck the lines with expectTypeOf simply do not execute as type checks: an ordinary pass ignores them, the test goes green, and a false sense arises that the type contract is protected. So type tests are set up as a separate script or a separate project with typecheck enabled and are run in CI without fail - otherwise they are silently useless.
The value of such a test is an early signal of a broken contract. A library's public function, an API response type, a generic helper - all of these are promises expressed in types. expectTypeOf turns a promise into a checkable assertion: widen a parameter, lose a field, break a generic's inference - and the type test goes red in CI rather than surfacing as a bug for whoever uses that type.
The typical failure is writing type tests and forgetting about --typecheck: they sit in the repository, look like protection, but have never executed and check nothing. The second failure is leaving the deprecated toMatchTypeOf in the code: it still works but is marked deprecated and will one day vanish. Write toExtend for assignability and toMatchObjectType for shape, and be sure to enable typecheck in the pipeline.
// user.test-d.ts - run: vitest --typecheck
import { expectTypeOf, assertType } from 'vitest'
expectTypeOf(getUser).parameter(0).toEqualTypeOf<string>()
expectTypeOf(getUser).returns.toEqualTypeOf<Promise<User>>()
expectTypeOf<AdminUser>().toExtend<User>() // subtype
assertType<Promise<User>>(getUser('u1')) // result type