У типов бывают собственные регрессии, которые обычный тест не видит. Функция может возвращать корректное значение в рантайме, но её сигнатура молча разъехалась: параметр стал шире, поле пропало из выводимого типа, дженерик перестал сужать. Vitest умеет проверять и это - у него есть тестирование типов, которое исполняется компилятором, а не движком, и ловит поломки контракта на уровне типов до того, как их заметит пользователь API.
Центральный инструмент - expectTypeOf. Он ничего не делает в рантайме: это утверждение, которое проверяет tsc. expectTypeOf(fn).toEqualTypeOf<T>() требует точного совпадения типов, а toExtend<T>() - что тип присваиваем к целевому, то есть является его подтипом. Важная деталь версии: прежний toMatchTypeOf устарел, вместо него теперь toExtend, а для сравнения по форме объекта - toMatchObjectType.
Помимо равенства, expectTypeOf умеет разбирать функцию по частям. .parameter(0) достаёт тип первого аргумента, .returns - тип результата, .toBeCallableWith(...) проверяет, что функцию можно вызвать с данным набором аргументов. Это позволяет утверждать контракт точечно: не весь тип целиком, а именно ту его грань, регрессию в которой вы хотите поймать, - например, что первый параметр остался строкой, а не расширился до string | number.
Рядом стоит assertType - более простое утверждение: assertType<T>(value) проверяет, что выражение имеет ожидаемый тип, и падает компиляцией, если нет. Его удобно применять к результату вызова, когда достаточно убедиться, что тип на выходе именно такой, а разбирать сигнатуру по частям не нужно. Оба инструмента работают в компиляции: если тип не сходится, тест не запустится - его отвергнет tsc.
Ключевая особенность - как это исполняется. Проверки типов не гоняет обычный прогон: они включаются флагом vitest --typecheck. Под ним Vitest запускает tsc по файлам тестов типов, и красным становится не упавший assert, а ошибка компилятора. Файлы для таких тестов обычно называют с суффиксом .test-d.ts, чтобы отделить их от рантайм-тестов и настроить на них отдельный include. Сам механизм в документации помечен экспериментальным, как и бенчмарки: он работает, но его опции могут меняться между версиями.
Отсюда вытекает и главный подводный камень. Без --typecheck строки с expectTypeOf просто не исполняются как проверки типов: обычный прогон их проигнорирует, тест позеленеет, и появится ложное чувство, что контракт типов под защитой. Поэтому тайп-тесты заводят отдельным скриптом или отдельным проектом с включённым typecheck и обязательно гоняют в CI - иначе они молчаливо бесполезны.
Ценность такого теста - в раннем сигнале о сломанном контракте. Публичная функция библиотеки, тип ответа API, дженерик-хелпер - всё это обещания, выраженные в типах. expectTypeOf превращает обещание в проверяемое утверждение: расширил параметр, потерял поле, сломал вывод дженерика - и тайп-тест краснеет в CI, а не всплывает багом у того, кто этим типом пользуется.
Типичный провал - написать тайп-тесты и забыть про --typecheck: они лежат в репозитории, выглядят как защита, но ни разу не исполнялись и ничего не проверяют. Второй провал - оставить в коде устаревший toMatchTypeOf: он ещё работает, но помечен deprecated и однажды исчезнет. Пишите toExtend для присваиваемости и toMatchObjectType для формы, и обязательно включите typecheck в пайплайн.
// user.test-d.ts - запуск: vitest --typecheck
import { expectTypeOf, assertType } from 'vitest'
expectTypeOf(getUser).parameter(0).toEqualTypeOf<string>()
expectTypeOf(getUser).returns.toEqualTypeOf<Promise<User>>()
expectTypeOf<AdminUser>().toExtend<User>() // подтип
assertType<Promise<User>>(getUser('u1')) // тип результата