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

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

Стандартный подход заключается в создании файла 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. Пользовательское расширение происходит поверх существующей структуры.

Механизм расширения 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 строго фильтрует переменные окружения: в клиентский код попадают только те, что начинаются с префикса VITE_. Это не только соглашение, но и механизм безопасности.

Пример .env:

VITE_API_URL=https://api.example.com
VITE_FEATURE_FLAG=true
SECRET_KEY=12345

В рантайме:

  • VITE_API_URL доступна в import.meta.env
  • VITE_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)
}

Разделение типов по режимам (mode)

Vite поддерживает несколько режимов сборки, что влияет на набор доступных переменных:

  • development
  • production
  • custom mode

Типизация может учитывать различия через условные типы:

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
}

Расширение ImportMeta через декларации

Помимо ImportMetaEnv, можно типизировать сам объект import.meta:

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Это фиксирует контракт между runtime и TypeScript, исключая обращения к несуществующим полям:

import.meta.env.VITE_UNKNOWN // ошибка компиляции

Пользовательские конфигурации поверх env

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

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')

Это объединяет статическую и динамическую проверку.

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

В больших приложениях окружение делится по доменам:

  • api
  • analytics
  • feature flags
  • build config

Каждый модуль может расширять ImportMetaEnv локально:

declare module 'vite/client' {
  interface ImportMetaEnv {
    readonly VITE_ANALYTICS_ID: string
  }
}

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

Особенности взаимодействия с import.meta.env в runtime

TypeScript-типизация существует только на этапе компиляции. В runtime import.meta.env — обычный объект, сформированный Vite на этапе сборки.

Это означает:

  • нельзя динамически добавлять поля с типовой безопасностью
  • нельзя рассчитывать на автоматическую проверку значений
  • типы не влияют на итоговый bundle

Поэтому типизация выполняет роль контракта, а не механизма исполнения.

Практика строгого контракта окружения

При дисциплинированной архитектуре часто используется подход “явного контракта”:

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.

Этот подход особенно важен при увеличении числа переменных и усложнении конфигурации сборки.