如何在 TypeScript 中为环境变量添加类型
你已经第一百次写下 process.env.API_KEY,但 TypeScript 仍然将其类型标注为 string | undefined。你的 IDE 没有提供任何自动补全。更糟糕的是,你部署到生产环境后才发现缺少的变量导致应用崩溃。TypeScript 环境变量值得更好的处理方式。
本指南将向你展示如何在现代前端和全栈项目中为环境变量添加类型安全——涵盖基于 Vite 的设置中的 import.meta.env 和 Node 环境中的 process.env。
核心要点
- 浏览器应用使用构建时注入(变量被打包到 bundle 中),而 Node.js 在运行时读取环境变量——这种区别会影响安全性和类型策略。
- 对于 Vite 项目使用
ImportMetaEnv声明,对于 Node 环境使用NodeJS.ProcessEnv扩展,以获得 IDE 自动补全和类型检查。 - 仅靠 TypeScript 类型无法防止运行时崩溃——始终在启动时使用简单检查或像 Zod 这样的模式库来验证必需的环境变量。
- 永远不要将机密信息放在带前缀的变量中(
VITE_、NEXT_PUBLIC_),因为这些变量会在客户端 bundle 中可见。
理解构建时与运行时环境变量
在添加类型之前,先理解一个关键区别:浏览器应用和服务器处理环境变量的方式不同。
构建时注入(浏览器应用): 像 Vite 这样的工具在构建期间将环境变量引用替换为实际值。这些变量在运行时并不存在——它们被打包到你的 JavaScript bundle 中。
运行时环境变量(服务器端): Node.js 在代码执行时从实际的系统环境中读取 process.env。值可以在不重新构建的情况下在部署之间更改。
这种区别对安全性很重要。任何在构建时注入的变量都会在客户端代码中可见。这就是为什么框架使用基于前缀的暴露规则——Vite 只暴露以 VITE_ 开头的变量,Next.js 使用 NEXT_PUBLIC_ 前缀暴露客户端变量。没有这些前缀的私钥仅保留在服务器端。
在 Vite 项目中为 import.meta.env 添加类型
Vite 使用 import.meta.env 而不是 process.env。要在 TypeScript 中添加类型安全的环境变量,创建一个声明文件:
// env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_TITLE: string
// 在此添加更多变量
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
现在 TypeScript 会提供自动补全,并将这些变量视为 string 而不是 string | undefined。将此文件放在你的 src 目录中,并确保你的 tsconfig.json 包含它。
为 Node.js 环境添加 ProcessEnv 类型
对于服务器端代码或使用 process.env 的工具,扩展 NodeJS.ProcessEnv 接口:
// globals.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string
API_SECRET: string
NODE_ENV: 'development' | 'production' | 'test'
}
}
这种方法可以在整个代码库中提供自动补全。NODE_ENV 示例展示了你可以为具有已知可能值的变量使用联合类型。
Discover how at OpenReplay.com.
为什么仅有类型还不够
问题在于:声明合并告诉 TypeScript 应该 存在什么,而不是 实际 存在什么。你已经将 DATABASE_URL 类型标注为 string,但如果有人忘记设置它,你的应用会在运行时崩溃。
TypeScript 类型在编译时会被擦除。它们无法验证环境变量在代码运行时是否实际存在。
在启动时验证环境变量
要在 TypeScript 中验证环境变量,请尽早检查它们——在应用执行任何重要操作之前。一个简单的方法:
// config.ts
function getEnvVar(key: string): string {
const value = process.env[key]
if (!value) {
throw new Error(`Missing required environment variable: ${key}`)
}
return value
}
export const config = {
databaseUrl: getEnvVar('DATABASE_URL'),
apiSecret: getEnvVar('API_SECRET'),
} as const
在应用的入口点导入这个配置模块。如果缺少任何变量,你会立即知道,而不是在请求处理过程中才发现。
对于更强大的验证,像 Zod 这样的库允许你定义验证类型、格式和约束的模式:
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.string().transform(Number).pipe(z.number().min(1)),
})
export const env = envSchema.parse(process.env)
如果 DATABASE_URL 不是有效的 URL 或 PORT 不是数字,这会快速失败并提供清晰的错误消息。
保护你的变量安全
记住前缀规则。在 Vite 项目中,只有 VITE_ 前缀的变量会到达浏览器。其他所有变量仅在构建过程(服务器端)中可用,不会发送到浏览器。永远不要将机密信息放在带前缀的变量中——它们会在生产 bundle 中可见。
对于服务器端机密,依赖托管平台的环境配置,而不是生产环境中的 .env 文件。立即将 .env 添加到你的 .gitignore 中。
结论
TypeScript 中的类型安全环境变量需要两层保护:用于编译时自动补全的声明合并和用于生产安全的运行时验证。对 Vite 项目使用 ImportMetaEnv,对 Node 环境使用 NodeJS.ProcessEnv,并始终在启动时验证必需的变量。你未来的自己——在凌晨 3 点调试生产事故时——会感谢你。
常见问题
可以,但要保持声明分离。为你的 Vite 前端包创建一个包含 ImportMetaEnv 的 env.d.ts 文件,为后端包创建一个包含 NodeJS.ProcessEnv 扩展的 globals.d.ts 文件。每个包的 tsconfig.json 应该只包含其相关的声明文件,以避免类型冲突。
你的 tsconfig.json 可能没有包含声明文件。检查你的 include 数组是否覆盖了文件位置,或者显式添加文件路径。还要验证你没有在另一个声明文件中意外覆盖该接口。进行更改后重启 TypeScript 服务器。
对于前端应用,构建时验证更有用,因为变量在构建过程中被打包进去。添加一个 prebuild 脚本,在 Vite 运行之前检查所需的 VITE_ 变量是否存在。浏览器中的运行时验证无论如何都无法从缺失的变量中恢复。
在接口声明中用问号标记可选变量,如 OPTIONAL_VAR?: string。对于验证,使用 Zod 的 optional() 方法或使用 default() 提供默认值。你的配置对象应该反映哪些变量是真正必需的,哪些是可选的。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.