类型安全的环境变量
构建类型安全的环境变量管理
概述
环境变量是应用配置的核心载体,用于存储 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.json的include中包含该文件。 - 默认值策略:为可选环境变量提供合理的默认值,避免应用因缺少环境变量而崩溃。但安全相关的变量(如 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');
}