Конфигурация в Vite тесно интегрирована с TypeScript. Даже если основной проект написан на чистом JavaScript, сама конфигурация может использовать типизацию, автодополнение, проверку ошибок и интеллектуальный анализ IDE.
Типизация особенно важна в крупных проектах, где конфигурация содержит:
Файл конфигурации может быть написан в следующих вариантах:
vite.config.js
vite.config.mjs
vite.config.cjs
vite.config.ts
vite.config.mts
vite.config.cts
Наиболее распространённый вариант — vite.config.ts.
Основной механизм типизации строится вокруг функции
defineConfig.
Пример:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000
}
})
Функция defineConfig предоставляет:
Без defineConfig TypeScript воспринимает объект менее
строго.
Без типизации:
export default {
servr: {
port: 3000
}
}
Ошибка в servr может остаться незамеченной.
С defineConfig:
import { defineConfig } from 'vite'
export default defineConfig({
servr: {
port: 3000
}
})
TypeScript сразу сообщит:
Object literal may only specify known properties
Это снижает вероятность ошибок в конфигурации.
Внутри Vite существует основной интерфейс конфигурации:
UserConfig
Он описывает всю структуру конфигурационного объекта.
Пример явной типизации:
import type { UserConfig } from 'vite'
const config: UserConfig = {
server: {
port: 5173
}
}
export default config
Однако чаще используется defineConfig, потому что он
лучше сохраняет вывод типов.
defineConfig использует generic-механизмы
TypeScript.
Упрощённо:
function defineConfig(config: UserConfig): UserConfig
Но в реальности сигнатура сложнее и позволяет:
Пример:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
host: '0.0.0.0',
port: 3000,
strictPort: true,
open: true
}
})
IDE автоматически показывает допустимые поля:
hostporthttpsproxyheaderscorswatchmiddlewareModeОшибка типа:
server: {
port: '3000'
}
TypeScript сообщит:
Type 'string' is not assignable to type 'number'
export default defineConfig({
build: {
outDir: 'dist',
minify: 'esbuild',
sourcemap: true,
cssCodeSplit: false
}
})
Поддерживаются строгие типы:
minify: 'esbuild'
minify: 'terser'
minify: false
Некорректное значение:
minify: 'webpack'
вызывает ошибку типизации.
import path from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
Типизация помогает избежать:
Плагины в Vite используют интерфейс:
Plugin
Пример:
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue()]
})
TypeScript проверяет:
Пример:
import type { Plugin } from 'vite'
function myPlugin(): Plugin {
return {
name: 'my-plugin',
transform(code, id) {
return code
}
}
}
Преимущества:
transform(code, id) {
return {
code,
map: null
}
}
TypeScript знает:
code: string
id: string
и проверяет допустимый результат:
TransformResult
import type { Plugin } from 'vite'
function devPlugin(): Plugin {
return {
name: 'dev-plugin',
configureServer(server) {
server.middlewares.use((req, res, next) => {
next()
})
}
}
}
Параметр server имеет тип:
ViteDevServer
Доступны подсказки:
middlewareswswatcherconfigtransformRequestmoduleGraphПо умолчанию:
import.meta.env
имеет базовую типизацию.
Для расширения создаётся файл:
env.d.ts
Пример:
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Теперь TypeScript проверяет:
import.meta.env.VITE_API_URL
и предупреждает о несуществующих переменных.
Ошибка:
import.meta.env.API_URL
Поскольку отсутствует префикс VITE_, TypeScript
сообщит:
Property 'API_URL' does not exist
Функциональная конфигурация:
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
return {
define: {
__MODE__: JSON.stringify(mode)
}
}
})
Тип mode:
string
Обычно используются:
development
production
test
staging
export default defineConfig(({ command }) => {
console.log(command)
return {}
})
Тип:
'build' | 'serve'
TypeScript контролирует допустимые варианты.
export default defineConfig(({ command }) => {
if (command === 'build') {
return {
build: {
sourcemap: true
}
}
}
return {
server: {
port: 3000
}
}
})
TypeScript корректно выводит типы для обеих веток.
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
define: {
__API__: JSON.stringify(env.VITE_API)
}
}
})
loadEnv возвращает:
Record<string, string>
Поэтому дополнительные проверки могут быть полезны.
Можно создать собственный тип:
interface AppEnv {
VITE_API: string
VITE_PORT: string
}
const env = loadEnv(mode, process.cwd()) as AppEnv
Теперь IDE понимает конкретные поля.
define: {
__DEV__: true,
__VERSION__: JSON.stringify('1.0.0')
}
Поле define типизировано как:
Record<string, any>
Но Vite ожидает значения, пригодные для текстовой подстановки.
Обычно используются:
css: {
modules: {
localsConvention: 'camelCaseOnly'
}
}
TypeScript проверяет допустимые значения:
camelCase
camelCaseOnly
dashes
dashesOnly
css: {
preprocessorOptions: {
scss: {
additionalData: `$injectedColor: orange;`
}
}
}
Автодополнение зависит от типа препроцессора.
optimizeDeps: {
include: ['lodash'],
exclude: ['moment']
}
Типы:
string[]
Но внутренние структуры сложнее и содержат дополнительные параметры анализа зависимостей.
ssr: {
noExternal: ['lodash']
}
TypeScript подсказывает допустимые SSR-поля:
externalnoExternaltargetformatworker: {
format: 'es'
}
Допустимые значения:
es
iife
preview: {
port: 5000
}
preview использует типы, похожие на server,
но со своими ограничениями.
Современный TypeScript позволяет использовать оператор:
satisfies
Пример:
import type { UserConfig } from 'vite'
export default {
server: {
port: 3000
}
} satisfies UserConfig
Преимущества:
Небезопасный вариант:
export default {
invalid: true
} as UserConfig
as принудительно приводит тип и может скрыть ошибки.
Безопасный вариант:
export default {
invalid: true
} satisfies UserConfig
TypeScript обнаружит ошибку.
Можно создавать собственные обёртки:
import type { UserConfig } from 'vite'
function createConfig<T extends UserConfig>(config: T): T {
return config
}
export default createConfig({
server: {
port: 3000
}
})
Это полезно при построении внутренних abstraction-layer.
Vite предоставляет функцию:
mergeConfig
Пример:
import { defineConfig, mergeConfig } from 'vite'
const baseConfig = defineConfig({
server: {
port: 3000
}
})
const productionConfig = defineConfig({
build: {
minify: 'esbuild'
}
})
export default mergeConfig(baseConfig, productionConfig)
TypeScript сохраняет типы обеих конфигураций.
Крупные проекты часто разбивают конфигурацию:
config/
server.ts
build.ts
plugins.ts
Пример:
import type { UserConfig } from 'vite'
export const buildConfig: UserConfig['build'] = {
sourcemap: true
}
Такой подход позволяет типизировать отдельные секции независимо.
import type { PluginOption } from 'vite'
Тип:
PluginOption
учитывает:
Пример:
const plugins: PluginOption[] = []
plugins: [
isDev && myPlugin()
]
TypeScript допускает:
false | Plugin
потому что PluginOption учитывает falsy-значения.
Можно расширять конфигурацию собственными полями:
interface CustomConfig {
apiUrl: string
}
Однако Vite не знает о них автоматически.
Для расширения используется module augmentation.
Пример:
declare module 'vite' {
interface UserConfig {
apiUrl?: string
}
}
Теперь:
export default defineConfig({
apiUrl: 'https://api.local'
})
не вызывает ошибок.
Сигнатура:
defineConfig((env) => {})
где:
env: ConfigEnv
Структура:
interface ConfigEnv {
command: 'build' | 'serve'
mode: string
isSsrBuild?: boolean
isPreview?: boolean
}
Vite поддерживает async config:
export default defineConfig(async ({ mode }) => {
const data = await loadSomething()
return {
define: {
__DATA__: JSON.stringify(data)
}
}
})
TypeScript корректно выводит:
Promise<UserConfig>
В конфигурации часто используются Node.js API:
import path from 'node:path'
import fs from 'node:fs'
Для корректной типизации требуется:
npm install -D @types/node
и настройка:
{
"compilerOptions": {
"types": ["node"]
}
}
Файл vite.config.ts компилируется отдельно от
приложения.
Особенно важны:
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node"
}
}
Для современных версий часто используется:
{
"moduleResolution": "Bundler"
}
В monorepo часто создают shared-config:
import type { UserConfig } from 'vite'
export function createBaseConfig(): UserConfig {
return {
server: {
port: 3000
}
}
}
Затем:
export default defineConfig({
...createBaseConfig()
})
Можно типизировать helper-функции:
interface AliasOptions {
find: string
replacement: string
}
Пример:
function createAlias(path: string): AliasOptions {
return {
find: '@',
replacement: path
}
}
Vite предоставляет встроенную типизацию:
const modules = import.meta.glob('./modules/*.ts')
Тип результата:
Record<string, () => Promise<unknown>>
Можно уточнить тип:
const modules = import.meta.glob<{ default: string }>('./modules/*.ts')
const modules = import.meta.glob('./modules/*.ts', {
eager: true
})
Теперь значения становятся модулями напрямую, а не функциями-загрузчиками.
TypeScript меняет вывод типов автоматически.
HMR API также типизирован:
if (import.meta.hot) {
import.meta.hot.accept()
}
Доступны методы:
acceptdisposeinvalidateonsendconst worker = new Worker(
new URL('./worker.ts', import.meta.url),
{
type: 'module'
}
)
TypeScript понимает:
import.meta.url;Проблема:
const path = require('path')
В ESM-конфигурации TypeScript может выдавать ошибки.
Современный Vite ориентирован на:
import path from 'node:path'
TypeScript поддерживает:
vite.config.mts
vite.config.cts
Разница:
.mts — ESM;.cts — CommonJS.Типизация зависит от режима модулей.
Можно отдельно проверять конфигурацию:
tsc --noEmit
TypeScript анализирует:
Максимальную пользу типизация даёт при:
{
"strict": true
}
Особенно полезны:
{
"noImplicitAny": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true
}
Наиболее заметные преимущества:
В крупных Vite-проектах TypeScript превращает конфигурацию из обычного объекта настроек в полноценный типизированный API, сопоставимый по уровню строгости с основным кодом приложения.