Vitest берёт трансформацию из Vite, а Vite использует esbuild, который на лету снимает типы и превращает TypeScript и JSX в исполнимый JavaScript. Поэтому для запуска тестов на TypeScript не нужен ни отдельный ts-jest, ни ручная связка с Babel: код исполняется тем же конвейером, что и приложение. Это убирает целый класс расхождений между тем, как модуль собирается для прода и как для теста. На больших проектах это ещё и заметно быстрее: esbuild трансформирует на порядок шустрее полноценного компилятора, а проверку типов вы всё равно запускаете отдельным шагом.
Важно понимать границу: снятие типов - это не проверка типов. esbuild выбрасывает аннотации, не сверяя их, ради скорости. Значит, тест с ошибкой типов - несуществующее поле, неверная сигнатура - вполне может пройти зелёным, потому что до проверки типов дело просто не доходит. Раннер отвечает за поведение во время выполнения, а не за корректность типов.
Отсюда простое разделение обязанностей: проверку типов выносят в отдельный шаг tsc --noEmit и ставят его в CI рядом с тестами. Запуск тестов не заменяет компилятор, а компилятор не запускает тесты - это два независимых стража. В package.json удобно держать test, test:run и typecheck как отдельные скрипты, чтобы пайплайн вызывал каждый явно.
ESM Vitest понимает нативно. С type: module в package.json тесты и код используют import и export без CommonJS-обёрток и без require. Это тот же формат модулей, что и в современном приложении, поэтому поведение импортов, порядок исполнения и tree-shaking совпадают с продом, а не моделируются приблизительно.
Ключевое удобство в том, что тот же Vite plugin, который трансформирует production-код, доступен и тестам. Алиасы, tsconfig paths (через vite-tsconfig-paths), JSX и виртуальные модули проекта работают в тестах без второй конфигурации. Не нужно описывать и синхронизировать два набора правил - источник трансформации один. Если плагин объявляет виртуальный модуль или подмену ассета, тест видит их ровно так же, как приложение, и переопределять их второй раз не приходится.
Один источник правды по сборке - это меньше сюрпризов. Если приложение собирается конкретным плагином, тесты видят ровно тот же результат его работы. Классическое 'работает в тестах, ломается в проде' чаще всего рождается именно из двух разных transform-конфигураций, которые со временем разошлись; Vite-native подход эту трещину закрывает по построению.
На практике держите один vitest.config, наследующий или переиспользующий Vite config приложения, а typecheck выносите отдельным скриптом. Не дублируйте transform в тестовой конфигурации и не добавляйте параллельный компилятор ради тестов - это ровно та лишняя конфигурация, которая потом разъезжается с основной.
Типичный провал - принимать зелёные тесты за доказательство того, что типы в порядке. Они этого не доказывают: при сломанных типах suite остаётся зелёным, и ошибка всплывает уже в сборке или в проде. Добавьте tsc --noEmit в пайплайн, а по возможности и в pre-commit, чтобы регрессия сигнатур ловилась до, а не после мержа.
{
"type": "module",
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"typecheck": "tsc --noEmit"
}
}import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'
// Один и тот же transform и алиасы, что и у приложения.
export default defineConfig({
plugins: [tsconfigPaths()],
test: { environment: 'node' },
})