Типизация конфигурации через TypeScript

Конфигурация в Vite тесно интегрирована с TypeScript. Даже если основной проект написан на чистом JavaScript, сама конфигурация может использовать типизацию, автодополнение, проверку ошибок и интеллектуальный анализ IDE.

Типизация особенно важна в крупных проектах, где конфигурация содержит:

  • множество плагинов;
  • условную логику;
  • разные режимы сборки;
  • пользовательские поля;
  • динамическое формирование настроек;
  • интеграцию с Node.js API.

Файл конфигурации может быть написан в следующих вариантах:

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 предоставляет:

  • полную типизацию объекта конфигурации;
  • подсказки IDE;
  • проверку корректности полей;
  • автодополнение вложенных параметров;
  • контроль допустимых значений.

Без defineConfig TypeScript воспринимает объект менее строго.


Почему defineConfig важнее обычного объекта

Без типизации:

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

Это снижает вероятность ошибок в конфигурации.


Тип UserConfig

Внутри Vite существует основной интерфейс конфигурации:

UserConfig

Он описывает всю структуру конфигурационного объекта.

Пример явной типизации:

import type { UserConfig } from 'vite'

const config: UserConfig = {
  server: {
    port: 5173
  }
}

export default config

Однако чаще используется defineConfig, потому что он лучше сохраняет вывод типов.


defineConfig и вывод типов

defineConfig использует generic-механизмы TypeScript.

Упрощённо:

function defineConfig(config: UserConfig): UserConfig

Но в реальности сигнатура сложнее и позволяет:

  • сохранять литеральные типы;
  • корректно выводить plugin-типизацию;
  • работать с функциями-конфигурациями;
  • учитывать условные режимы.

Типизация server

Пример:

import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    host: '0.0.0.0',
    port: 3000,
    strictPort: true,
    open: true
  }
})

IDE автоматически показывает допустимые поля:

  • host
  • port
  • https
  • proxy
  • headers
  • cors
  • watch
  • middlewareMode

Ошибка типа:

server: {
  port: '3000'
}

TypeScript сообщит:

Type 'string' is not assignable to type 'number'

Типизация build

export default defineConfig({
  build: {
    outDir: 'dist',
    minify: 'esbuild',
    sourcemap: true,
    cssCodeSplit: false
  }
})

Поддерживаются строгие типы:

minify: 'esbuild'
minify: 'terser'
minify: false

Некорректное значение:

minify: 'webpack'

вызывает ошибку типизации.


Типизация resolve.alias

import path from 'node:path'
import { defineConfig } from 'vite'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
})

Типизация помогает избежать:

  • неправильных структур alias;
  • ошибочных путей;
  • неверного формата массива.

Типизация plugins

Плагины в Vite используют интерфейс:

Plugin

Пример:

import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue()]
})

TypeScript проверяет:

  • корректность plugin-объекта;
  • наличие hook-методов;
  • совместимость возвращаемых значений.

Типизация пользовательского плагина

Пример:

import type { Plugin } from 'vite'

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',

    transform(code, id) {
      return code
    }
  }
}

Преимущества:

  • строгая сигнатура hook-функций;
  • подсказки по параметрам;
  • проверка возвращаемых типов.

Типизация transform

transform(code, id) {
  return {
    code,
    map: null
  }
}

TypeScript знает:

code: string
id: string

и проверяет допустимый результат:

TransformResult

Типизация configureServer

import type { Plugin } from 'vite'

function devPlugin(): Plugin {
  return {
    name: 'dev-plugin',

    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        next()
      })
    }
  }
}

Параметр server имеет тип:

ViteDevServer

Доступны подсказки:

  • middlewares
  • ws
  • watcher
  • config
  • transformRequest
  • moduleGraph

Типизация env-переменных

По умолчанию:

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

и предупреждает о несуществующих переменных.


Проверка типов env

Ошибка:

import.meta.env.API_URL

Поскольку отсутствует префикс VITE_, TypeScript сообщит:

Property 'API_URL' does not exist

Типизация mode

Функциональная конфигурация:

import { defineConfig } from 'vite'

export default defineConfig(({ mode }) => {
  return {
    define: {
      __MODE__: JSON.stringify(mode)
    }
  }
})

Тип mode:

string

Обычно используются:

development
production
test
staging

Типизация command

export default defineConfig(({ command }) => {
  console.log(command)

  return {}
})

Тип:

'build' | 'serve'

TypeScript контролирует допустимые варианты.


Типизация conditional config

export default defineConfig(({ command }) => {
  if (command === 'build') {
    return {
      build: {
        sourcemap: true
      }
    }
  }

  return {
    server: {
      port: 3000
    }
  }
})

TypeScript корректно выводит типы для обеих веток.


Типизация loadEnv

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>

Поэтому дополнительные проверки могут быть полезны.


Усиление типизации env

Можно создать собственный тип:

interface AppEnv {
  VITE_API: string
  VITE_PORT: string
}

const env = loadEnv(mode, process.cwd()) as AppEnv

Теперь IDE понимает конкретные поля.


Типизация define

define: {
  __DEV__: true,
  __VERSION__: JSON.stringify('1.0.0')
}

Поле define типизировано как:

Record<string, any>

Но Vite ожидает значения, пригодные для текстовой подстановки.

Обычно используются:

  • строки;
  • boolean;
  • сериализованные JSON-значения.

Типизация css

css: {
  modules: {
    localsConvention: 'camelCaseOnly'
  }
}

TypeScript проверяет допустимые значения:

camelCase
camelCaseOnly
dashes
dashesOnly

Типизация css.preprocessorOptions

css: {
  preprocessorOptions: {
    scss: {
      additionalData: `$injectedColor: orange;`
    }
  }
}

Автодополнение зависит от типа препроцессора.


Типизация optimizeDeps

optimizeDeps: {
  include: ['lodash'],
  exclude: ['moment']
}

Типы:

string[]

Но внутренние структуры сложнее и содержат дополнительные параметры анализа зависимостей.


Типизация ssr

ssr: {
  noExternal: ['lodash']
}

TypeScript подсказывает допустимые SSR-поля:

  • external
  • noExternal
  • target
  • format

Типизация worker

worker: {
  format: 'es'
}

Допустимые значения:

es
iife

Типизация preview

preview: {
  port: 5000
}

preview использует типы, похожие на server, но со своими ограничениями.


Использование satisfies

Современный TypeScript позволяет использовать оператор:

satisfies

Пример:

import type { UserConfig } from 'vite'

export default {
  server: {
    port: 3000
  }
} satisfies UserConfig

Преимущества:

  • строгая проверка структуры;
  • сохранение литеральных типов;
  • отсутствие лишнего расширения типов.

Разница между satisfies и as

Небезопасный вариант:

export default {
  invalid: true
} as UserConfig

as принудительно приводит тип и может скрыть ошибки.

Безопасный вариант:

export default {
  invalid: true
} satisfies UserConfig

TypeScript обнаружит ошибку.


Типизация через generics

Можно создавать собственные обёртки:

import type { UserConfig } from 'vite'

function createConfig<T extends UserConfig>(config: T): T {
  return config
}

export default createConfig({
  server: {
    port: 3000
  }
})

Это полезно при построении внутренних abstraction-layer.


Типизация mergeConfig

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
}

Такой подход позволяет типизировать отдельные секции независимо.


Типизация plugins-массива

import type { PluginOption } from 'vite'

Тип:

PluginOption

учитывает:

  • одиночный плагин;
  • массив;
  • Promise;
  • условные значения;
  • вложенные структуры.

Пример:

const plugins: PluginOption[] = []

Условные плагины и типизация

plugins: [
  isDev && myPlugin()
]

TypeScript допускает:

false | Plugin

потому что PluginOption учитывает falsy-значения.


Типизация custom fields

Можно расширять конфигурацию собственными полями:

interface CustomConfig {
  apiUrl: string
}

Однако Vite не знает о них автоматически.

Для расширения используется module augmentation.


Module augmentation

Пример:

declare module 'vite' {
  interface UserConfig {
    apiUrl?: string
  }
}

Теперь:

export default defineConfig({
  apiUrl: 'https://api.local'
})

не вызывает ошибок.


Типизация defineConfig callback

Сигнатура:

defineConfig((env) => {})

где:

env: ConfigEnv

Структура:

interface ConfigEnv {
  command: 'build' | 'serve'
  mode: string
  isSsrBuild?: boolean
  isPreview?: boolean
}

Типизация async-конфигурации

Vite поддерживает async config:

export default defineConfig(async ({ mode }) => {
  const data = await loadSomething()

  return {
    define: {
      __DATA__: JSON.stringify(data)
    }
  }
})

TypeScript корректно выводит:

Promise<UserConfig>

Использование Node.js типов

В конфигурации часто используются Node.js API:

import path from 'node:path'
import fs from 'node:fs'

Для корректной типизации требуется:

npm install -D @types/node

и настройка:

{
  "compilerOptions": {
    "types": ["node"]
  }
}

Влияние tsconfig на Vite config

Файл vite.config.ts компилируется отдельно от приложения.

Особенно важны:

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Node"
  }
}

Для современных версий часто используется:

{
  "moduleResolution": "Bundler"
}

Типизация monorepo-конфигураций

В monorepo часто создают shared-config:

import type { UserConfig } from 'vite'

export function createBaseConfig(): UserConfig {
  return {
    server: {
      port: 3000
    }
  }
}

Затем:

export default defineConfig({
  ...createBaseConfig()
})

Типизация alias helper

Можно типизировать helper-функции:

interface AliasOptions {
  find: string
  replacement: string
}

Пример:

function createAlias(path: string): AliasOptions {
  return {
    find: '@',
    replacement: path
  }
}

Типизация import.meta.glob

Vite предоставляет встроенную типизацию:

const modules = import.meta.glob('./modules/*.ts')

Тип результата:

Record<string, () => Promise<unknown>>

Можно уточнить тип:

const modules = import.meta.glob<{ default: string }>('./modules/*.ts')

Типизация import.meta.glob eager

const modules = import.meta.glob('./modules/*.ts', {
  eager: true
})

Теперь значения становятся модулями напрямую, а не функциями-загрузчиками.

TypeScript меняет вывод типов автоматически.


Типизация import.meta.hot

HMR API также типизирован:

if (import.meta.hot) {
  import.meta.hot.accept()
}

Доступны методы:

  • accept
  • dispose
  • invalidate
  • on
  • send

Типизация Web Worker через Vite

const worker = new Worker(
  new URL('./worker.ts', import.meta.url),
  {
    type: 'module'
  }
)

TypeScript понимает:

  • import.meta.url;
  • worker-модули;
  • типы URL API.

Ошибки при смешивании CommonJS и ESM

Проблема:

const path = require('path')

В ESM-конфигурации TypeScript может выдавать ошибки.

Современный Vite ориентирован на:

import path from 'node:path'

mts и cts

TypeScript поддерживает:

vite.config.mts
vite.config.cts

Разница:

  • .mts — ESM;
  • .cts — CommonJS.

Типизация зависит от режима модулей.


Проверка конфигурации через tsc

Можно отдельно проверять конфигурацию:

tsc --noEmit

TypeScript анализирует:

  • Vite config;
  • env typings;
  • plugin typings;
  • helper-функции;
  • augmentation-модули.

Строгий режим TypeScript

Максимальную пользу типизация даёт при:

{
  "strict": true
}

Особенно полезны:

{
  "noImplicitAny": true,
  "strictNullChecks": true,
  "exactOptionalPropertyTypes": true
}

Типизация и IDE

Наиболее заметные преимущества:

  • автодополнение конфигурации;
  • быстрый поиск ошибок;
  • навигация по API;
  • документация прямо в редакторе;
  • рефакторинг без поломок;
  • безопасное изменение больших конфигураций.

В крупных Vite-проектах TypeScript превращает конфигурацию из обычного объекта настроек в полноценный типизированный API, сопоставимый по уровню строгости с основным кодом приложения.