前置知识: TypeScript

类型安全的环境变量

3 minIntermediate2026/6/14

构建类型安全的环境变量管理

概述

环境变量是应用配置的核心载体,用于存储 API 地址、密钥、功能开关等运行时配置。传统的环境变量访问方式(如 process.env.VARIABLE)缺乏型安全,容易出现拼写错误和型不匹配。通过 TypeScript 的字面量型、映射型和泛型约束,可以构建型安全的环境变量管理系统,确保变量名正确、型精确、缺失变量在编译时被发现。

基础概念

环境变量声明:通过声明文件(.d.ts)定义环境变量的型,使 TypeScript 能够识别 process.env 上的自定义属性。

类型安全的访问器:封装环境变量读取逻辑,提供型安全的访问函数,自动处理型转换和缺失值检查。

验证函数:在应用启动时验证所有必需的环境变量是否存在且格式正确,避免运行时错误。

默认值机制:为可选环境变量提供型安全的默认值,确保访问结果始终有确定的型。

快速上手

环境变量型声明

// env.d.ts - 声明环境变量类型
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
  readonly VITE_DEBUG: string; // 'true' | 'false'
  readonly VITE_MAX_RETRIES: string; // 数字以字符串形式存储
  readonly VITE_ENABLE_ANALYTICS?: string; // 可选变量
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

// Node.js 环境
declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV: 'development' | 'production' | 'test';
    readonly PORT?: string;
    readonly DATABASE_URL: string;
    readonly JWT_SECRET: string;
    readonly REDIS_URL?: string;
  }
}

型安全的访问函数

// 读取字符串环境变量
function getEnv(key: keyof NodeJS.ProcessEnv): string {
  const value = process.env[key];
  if (value === undefined) {
    throw new Error(`环境变量 ${key} 未设置`);
  }
  return value;
}

// 读取可选字符串环境变量
function getOptionalEnv(key: keyof NodeJS.ProcessEnv, defaultValue: string): string {
  return process.env[key] ?? defaultValue;
}

// 读取布尔值环境变量
function getBooleanEnv(key: keyof NodeJS.ProcessEnv, defaultValue = false): boolean {
  const value = process.env[key];
  if (value === undefined) return defaultValue;
  return value === 'true' || value === '1';
}

// 读取数字环境变量
function getNumberEnv(key: keyof NodeJS.ProcessEnv, defaultValue: number): number {
  const value = process.env[key];
  if (value === undefined) return defaultValue;
  const parsed = parseInt(value, 10);
  if (isNaN(parsed)) {
    throw new Error(`环境变量 ${key} 不是有效的数字: ${value}`);
  }
  return parsed;
}

详细用法

型安全的环境变量配置对象

// 定义环境变量配置结构
interface EnvConfig {
  nodeEnv: 'development' | 'production' | 'test';
  port: number;
  database: {
    url: string;
    poolSize: number;
  };
  jwt: {
    secret: string;
    expiresIn: string;
  };
  redis: {
    url: string | null;
    ttl: number;
  };
  features: {
    debug: boolean;
    analytics: boolean;
  };
}

// 从环境变量构建配置对象
function loadConfig(): EnvConfig {
  return {
    nodeEnv: (process.env.NODE_ENV as EnvConfig['nodeEnv']) ?? 'development',
    port: getNumberEnv('PORT', 3000),
    database: {
      url: getEnv('DATABASE_URL'),
      poolSize: getNumberEnv('DB_POOL_SIZE', 10),
    },
    jwt: {
      secret: getEnv('JWT_SECRET'),
      expiresIn: getOptionalEnv('JWT_EXPIRES_IN', '7d'),
    },
    redis: {
      url: process.env.REDIS_URL ?? null,
      ttl: getNumberEnv('REDIS_TTL', 3600),
    },
    features: {
      debug: getBooleanEnv('DEBUG', false),
      analytics: getBooleanEnv('ENABLE_ANALYTICS', false),
    },
  };
}

// 使用
const config = loadConfig();
config.database.url; // string
config.redis.url; // string | null
config.features.debug; // boolean

环境变量验证

// 定义环境变量的验证规则
interface EnvValidationRule<T> {
  required: boolean;
  type: 'string' | 'number' | 'boolean' | 'url';
  default?: T;
  validate?: (value: T) => boolean;
  errorMessage?: string;
}

type EnvValidationSchema = Record<string, EnvValidationRule<any>>;

// 验证环境变量
function validateEnv(schema: EnvValidationSchema): void {
  const errors: string[] = [];

  for (const [key, rule] of Object.entries(schema)) {
    const value = process.env[key];

    // 检查必需变量
    if (rule.required && !value) {
      errors.push(`环境变量 ${key} 是必需的但未设置`);
      continue;
    }

    // 如果值不存在但有默认值,跳过验证
    if (!value && rule.default !== undefined) continue;

    // 类型验证
    if (value) {
      switch (rule.type) {
        case 'number':
          if (isNaN(Number(value))) {
            errors.push(`环境变量 ${key} 应为数字,实际值: ${value}`);
          }
          break;
        case 'boolean':
          if (!['true', 'false', '0', '1'].includes(value)) {
            errors.push(`环境变量 ${key} 应为布尔值,实际值: ${value}`);
          }
          break;
        case 'url':
          try {
            new URL(value);
          } catch {
            errors.push(`环境变量 ${key} 应为有效 URL,实际值: ${value}`);
          }
          break;
      }

      // 自定义验证
      if (rule.validate && !rule.validate(value as any)) {
        errors.push(rule.errorMessage ?? `环境变量 ${key} 验证失败`);
      }
    }
  }

  if (errors.length > 0) {
    throw new Error(`环境变量验证失败:\n${errors.join('\n')}`);
  }
}

// 使用
validateEnv({
  DATABASE_URL: { required: true, type: 'url' },
  JWT_SECRET: {
    required: true,
    type: 'string',
    validate: (v) => v.length >= 32,
    errorMessage: 'JWT_SECRET 至少需要 32 个字符',
  },
  PORT: { required: false, type: 'number', default: 3000 },
  DEBUG: { required: false, type: 'boolean', default: false },
});

Vite 环境变量

// vite-env.d.ts
/// <reference types="vite/client" />

interface ViteEnv {
  VITE_API_URL: string;
  VITE_APP_TITLE: string;
  VITE_DEBUG: string;
  VITE_ENABLE_ANALYTICS?: string;
}

// 类型安全的 Vite 环境变量访问
function getViteEnv(): ViteEnv {
  return {
    VITE_API_URL: import.meta.env.VITE_API_URL,
    VITE_APP_TITLE: import.meta.env.VITE_APP_TITLE,
    VITE_DEBUG: import.meta.env.VITE_DEBUG,
    VITE_ENABLE_ANALYTICS: import.meta.env.VITE_ENABLE_ANALYTICS,
  };
}

// .env 文件
// VITE_API_URL=https://api.example.com
// VITE_APP_TITLE=我的应用
// VITE_DEBUG=false

常见场景

多环境配置

// 按环境加载不同配置
type Environment = 'development' | 'staging' | 'production';

interface EnvironmentConfig {
  apiUrl: string;
  debug: boolean;
  logLevel: 'debug' | 'info' | 'warn' | 'error';
  enableAnalytics: boolean;
  sentryDsn: string | null;
}

const environmentConfigs: Record<Environment, EnvironmentConfig> = {
  development: {
    apiUrl: 'http://localhost:3000',
    debug: true,
    logLevel: 'debug',
    enableAnalytics: false,
    sentryDsn: null,
  },
  staging: {
    apiUrl: 'https://staging-api.example.com',
    debug: false,
    logLevel: 'info',
    enableAnalytics: true,
    sentryDsn: 'https://sentry.io/staging',
  },
  production: {
    apiUrl: 'https://api.example.com',
    debug: false,
    logLevel: 'warn',
    enableAnalytics: true,
    sentryDsn: 'https://sentry.io/production',
  },
};

function getEnvironmentConfig(): EnvironmentConfig {
  const env = (process.env.NODE_ENV ?? 'development') as Environment;
  return environmentConfigs[env];
}

运行时环境变量

// 前端运行时环境变量(通过 window.__ENV__ 注入)
interface RuntimeEnv {
  API_URL: string;
  WS_URL: string;
  VERSION: string;
}

declare global {
  interface Window {
    __ENV__: RuntimeEnv;
  }
}

function getRuntimeEnv(): RuntimeEnv {
  if (!window.__ENV__) {
    throw new Error('运行时环境变量未注入');
  }
  return window.__ENV__;
}

// 使用
const runtimeConfig = getRuntimeEnv();
runtimeConfig.API_URL; // string
runtimeConfig.VERSION; // string

注意事项

  • 敏感信息:环境变量中的敏感信息(密钥、密码)不应暴露到前端。Vite 中以 VITE_ 开头的变量会被打包到前端代码中,敏感变量不要使用此前缀。
  • 类型声明位置:环境变量型声明应放在 .d.ts 文件中,并在 tsconfig.jsoninclude 中包含该文件。
  • 默认值策略:为可选环境变量提供合理的默认值,避免应用因缺少环境变量而崩溃。但安全相关的变量(如 JWT_SECRET)不应有默认值。
  • 构建时 vs 运行时:Vite 的环境变量在构建时被静态替换。如果需要运行时动态配置,应使用 window.__ENV__ 等运行时注入方案。

进阶用法

Zod 验证

import { z } from 'zod';

// 使用 Zod 定义环境变量 Schema
const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
  DEBUG: z.coerce.boolean().default(false),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

// 验证并解析环境变量
const env = envSchema.parse(process.env);

// env 的类型自动推断
env.PORT; // number
env.DATABASE_URL; // string
env.REDIS_URL; // string | undefined
env.DEBUG; // boolean

// 安全解析(不抛出异常)
const result = envSchema.safeParse(process.env);
if (!result.success) {
  console.error('环境变量验证失败:', result.error.flatten().fieldErrors);
  process.exit(1);
}

环境变量文档生成

// 从验证 Schema 自动生成环境变量文档
type EnvDocEntry = {
  name: string;
  type: string;
  required: boolean;
  default?: string;
  description?: string;
};

function generateEnvDoc(schema: EnvValidationSchema): EnvDocEntry[] {
  return Object.entries(schema).map(([name, rule]) => ({
    name,
    type: rule.type,
    required: rule.required,
    default: rule.default?.toString(),
    description: rule.errorMessage,
  }));
}

// 生成 .env.example 文件
function generateEnvExample(schema: EnvValidationSchema): string {
  return Object.entries(schema)
    .map(([name, rule]) => {
      const comment = rule.required ? '# 必需' : '# 可选';
      const defaultVal = rule.default ? `=${rule.default}` : '=';
      return `${comment}\n${name}${defaultVal}`;
    })
    .join('\n\n');
}