Vite использует нативную поддержку TypeScript-типов через механизм
деклараций и расширения глобальных интерфейсов, что позволяет строго
типизировать как встроенные переменные окружения, так и пользовательские
значения, добавляемые в приложение через .env файлы или
runtime-конфигурации. Основная точка входа в типизацию окружения —
интерфейс ImportMetaEnv, который описывает структуру
import.meta.env.
В среде Vite переменные окружения доступны через
import.meta.env. Без явной типизации этот объект имеет
размытый тип, что приводит к потере контроля над доступными ключами и их
значениями.
Типовая модель выглядит следующим образом:
import.meta.env.MODE — режим сборки
(development, production, кастомный mode)import.meta.env.BASE_URL — базовый путь приложенияimport.meta.env.DEV — флаг режима разработкиimport.meta.env.PROD — флаг production-сборкиVITE_При использовании TypeScript ключевым механизмом становится расширение глобального интерфейса:
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_NAME: string
}
Однако одного объявления недостаточно — оно должно быть размещено в файле деклараций, который подхватывается компилятором.
Стандартный подход заключается в создании файла env.d.ts
в корне или внутри src:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_NAME: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Ключевая особенность — подключение vite/client, которое
уже содержит базовые типы ImportMetaEnv. Пользовательское
расширение происходит поверх существующей структуры.
TypeScript использует декларативное слияние интерфейсов. Это
означает, что при повторном объявлении ImportMetaEnv
происходит объединение типов.
Пример объединения:
interface ImportMetaEnv {
readonly VITE_API_URL: string
}
interface ImportMetaEnv {
readonly VITE_TOKEN: string
}
Фактический итоговый тип:
{
VITE_API_URL: string
VITE_TOKEN: string
}
Это позволяет разделять декларации по модулям, например, выделяя API-конфигурации, feature-flags и окружение сборки.
Vite строго фильтрует переменные окружения: в клиентский код попадают
только те, что начинаются с префикса VITE_. Это не только
соглашение, но и механизм безопасности.
Пример .env:
VITE_API_URL=https://api.example.com
VITE_FEATURE_FLAG=true
SECRET_KEY=12345
В рантайме:
VITE_API_URL доступна в
import.meta.envVITE_FEATURE_FLAG доступнаSECRET_KEY недоступна в клиентском кодеТипизация должна отражать именно эти публичные ключи:
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_FEATURE_FLAG: string
}
Даже если значение логически boolean или number, на уровне окружения оно всегда строка.
Все значения import.meta.env приходят как строки,
включая числа и булевы флаги. Поэтому типизация часто требует
логического уточнения на уровне приложения.
Типовой паттерн:
const isDebug = import.meta.env.VITE_DEBUG === 'true'
const apiUrl = import.meta.env.VITE_API_URL
Для числовых значений:
const timeout = Number(import.meta.env.VITE_TIMEOUT)
При строгой архитектуре допустимо описывать “логический смысл” переменных отдельно:
interface AppEnv {
apiUrl: string
debug: boolean
timeout: number
}
const env: AppEnv = {
apiUrl: import.meta.env.VITE_API_URL,
debug: import.meta.env.VITE_DEBUG === 'true',
timeout: Number(import.meta.env.VITE_TIMEOUT)
}
Vite поддерживает несколько режимов сборки, что влияет на набор доступных переменных:
Типизация может учитывать различия через условные типы:
type DevEnv = {
VITE_DEBUG: string
}
type ProdEnv = {
VITE_CDN_URL: string
}
interface ImportMetaEnv extends DevEnv, ProdEnv {}
Однако в реальных проектах чаще применяется единая структура с опциональными полями:
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_DEBUG?: string
}
Помимо ImportMetaEnv, можно типизировать сам объект
import.meta:
interface ImportMeta {
readonly env: ImportMetaEnv
}
Это фиксирует контракт между runtime и TypeScript, исключая обращения к несуществующим полям:
import.meta.env.VITE_UNKNOWN // ошибка компиляции
В реальных архитектурах переменные окружения часто оборачиваются в слой конфигурации:
const config = {
apiUrl: import.meta.env.VITE_API_URL,
appName: import.meta.env.VITE_APP_NAME,
debug: import.meta.env.VITE_DEBUG === 'true'
} as const
Типизация через as const фиксирует структуру
объекта:
type Config = typeof config
Это позволяет использовать единый источник правды вместо прямого
обращения к import.meta.env.
Для масштабируемых приложений применяется централизованный файл типов:
src/types/env.d.ts
Пример:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_VERSION: string
readonly VITE_SENTRY_DSN?: string
readonly VITE_ENABLE_MOCKS?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Такой подход позволяет контролировать контракт окружения на уровне компиляции.
Иногда требуется поддержка расширяемых ключей, например для feature flags. В этом случае используется индексная сигнатура:
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly [key: `VITE_FEATURE_${string}`]: string
}
Это даёт возможность описывать множество однотипных переменных:
VITE_FEATURE_CHAT=true
VITE_FEATURE_PAYMENTS=false
При этом сохраняется строгая структура базовых полей.
Типизация сама по себе не гарантирует наличие переменных в runtime. Поэтому часто добавляется слой валидации:
function requireEnv(key: keyof ImportMetaEnv): string {
const value = import.meta.env[key]
if (!value) {
throw new Error(`Missing env variable: ${key}`)
}
return value
}
Использование:
const apiUrl = requireEnv('VITE_API_URL')
Это объединяет статическую и динамическую проверку.
В больших приложениях окружение делится по доменам:
Каждый модуль может расширять ImportMetaEnv
локально:
declare module 'vite/client' {
interface ImportMetaEnv {
readonly VITE_ANALYTICS_ID: string
}
}
Такой подход позволяет масштабировать конфигурацию без монолитного файла типов.
TypeScript-типизация существует только на этапе компиляции. В runtime
import.meta.env — обычный объект, сформированный Vite на
этапе сборки.
Это означает:
Поэтому типизация выполняет роль контракта, а не механизма исполнения.
При дисциплинированной архитектуре часто используется подход “явного контракта”:
const envSchema = {
VITE_API_URL: import.meta.env.VITE_API_URL,
VITE_APP_NAME: import.meta.env.VITE_APP_NAME
} satisfies Record<string, string>
Ключевая идея — гарантировать, что все используемые переменные
перечислены явно, без скрытых обращений к
import.meta.env.
Этот подход особенно важен при увеличении числа переменных и усложнении конфигурации сборки.