TypeScript 與 Node.js:環境變數定義(process.env)實務應用
簡介
在 Node.js 與 TypeScript 的專案中,環境變數是管理 設定、機密資訊(例如資料庫連線字串、API 金鑰、Port)最常見且安全的方式。透過 process.env,我們可以在不同的執行環境(開發、測試、正式)間切換設定,而不必把敏感資訊寫死在程式碼裡。
對於使用 TypeScript 的開發者來說,process.env 的型別安全是一大挑戰:原生的 process.env 只是一個 NodeJS.ProcessEnv({ [key: string]: string | undefined }),若直接使用會失去 TypeScript 的靜態檢查優勢。本文將說明 如何在 TypeScript 專案中正確、有效率地使用環境變數,並提供多個實作範例、常見陷阱與最佳實踐,讓你在實務開發中既安全又便利。
核心概念
1. process.env 的基本使用
process.env 是 Node.js 提供的全域物件,代表執行程式時的環境變數集合。每個變數的值都是 字串(或 undefined),因此在 TypeScript 中直接取用時會失去類型資訊。
// 直接使用 process.env(缺乏型別安全)
const port = process.env.PORT; // type: string | undefined
console.log(`Server listening on ${port}`);
⚠️ 注意:若環境變數未設定,
port會是undefined,若直接傳給listen會導致程式在執行時拋錯。
2. 為環境變數建立型別宣告
為了讓 TypeScript 能在編譯階段檢查,我們可以自訂 環境變數的型別宣告檔(env.d.ts),將常用的變數列舉出來並指定型別。
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
/** 伺服器監聽埠號,預設 3000 */
readonly PORT?: string;
/** 資料庫連線字串 */
readonly DATABASE_URL: string;
/** 是否為正式環境 */
readonly NODE_ENV: 'development' | 'production' | 'test';
/** 第三方 API 金鑰 */
readonly API_KEY?: string;
}
}
重點:使用
readonly可以避免在程式碼中意外改寫環境變數,並且透過聯合型別(如NodeEnv)限制允許的字串集合。
3. 建立環境變數的「安全」讀取工具
即使有型別宣告,我們仍需處理 缺失或格式錯誤 的情況。以下提供一個簡潔的 config.ts,封裝環境變數的讀取與驗證。
// config.ts
import * as dotenv from 'dotenv';
// 先載入 .env 檔(如果存在)
dotenv.config();
type Env = {
port: number;
databaseUrl: string;
nodeEnv: 'development' | 'production' | 'test';
apiKey?: string;
};
/**
* 取得環境變數,若缺少必填項目會直接拋出錯誤
*/
function getEnv(): Env {
const {
PORT,
DATABASE_URL,
NODE_ENV,
API_KEY,
} = process.env;
if (!DATABASE_URL) {
throw new Error('❌ 環境變數 DATABASE_URL 必須設定!');
}
if (!NODE_ENV) {
throw new Error('❌ 環境變數 NODE_ENV 必須設定!');
}
const port = PORT ? Number(PORT) : 3000; // 預設 3000
if (Number.isNaN(port) || port <= 0) {
throw new Error('❌ PORT 必須是一個正整數!');
}
return {
port,
databaseUrl: DATABASE_URL,
nodeEnv: NODE_ENV as Env['nodeEnv'],
apiKey: API_KEY,
};
}
// 直接匯出已驗證的設定,供整個專案使用
export const env = getEnv();
範例說明
dotenv:在開發環境常用的套件,可將.env檔內容注入process.env。- 必填檢查:
DATABASE_URL、NODE_ENV為關鍵設定,缺少時即拋錯,避免程式在執行階段才失敗。 - 型別轉換:
PORT由字串轉為number,同時提供預設值。
4. 在程式中使用已封裝的 env
// server.ts
import http from 'http';
import { env } from './config';
const server = http.createServer((req, res) => {
res.end('Hello TypeScript + Node.js');
});
server.listen(env.port, () => {
console.log(`🚀 Server running on http://localhost:${env.port} (${env.nodeEnv})`);
});
好處:所有環境變數都已在
config.ts中驗證過,server.ts只需要關注業務邏輯,減少重複檢查的程式碼。
5. 多環境的 .env 檔管理
| 檔案名稱 | 說明 |
|---|---|
.env |
預設開發環境設定 |
.env.test |
測試環境(例如 Jest) |
.env.production |
正式環境(部署時使用) |
在部署腳本或 CI/CD 中,只需要設定 NODE_ENV,再由 dotenv 自動載入對應檔案:
// bootstrap.ts
import * as dotenv from 'dotenv';
import * as path from 'path';
const envFile = `.env.${process.env.NODE_ENV ?? 'development'}`;
dotenv.config({ path: path.resolve(__dirname, '..', envFile) });
6. 讓 TypeScript 編譯器認識 .env 變數
若使用 ESLint 或 Prettier,可能會出現「process.env 可能未定義」的警告。只要在 tsconfig.json 中加入 types 設定,即可讓編譯器載入自訂的型別宣告檔:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./src/types"]
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
直接使用 process.env |
缺乏型別安全,容易在執行時因 undefined 或字串未轉型而錯誤。 |
建立 型別宣告 (env.d.ts) 並封裝讀取函式 (config.ts)。 |
| 環境變數未轉型 | process.env.PORT 仍是字串,傳給 listen 需要 number。 |
在 config.ts 中 顯式轉型(Number())並檢查 NaN。 |
| 在程式碼中硬編碼環境變數名稱 | 若變數名稱改變,所有檔案都要手動更新。 | 使用 常數或介面(type EnvKeys = keyof NodeJS.ProcessEnv)集中管理。 |
.env 檔未加入 .gitignore |
機密資訊會被推到遠端儲存庫。 | 確認 .gitignore 包含 .env*,並在 CI 中使用 Secret 管理(GitHub Actions、GitLab CI)。 |
不同環境使用同一套 .env |
測試環境與正式環境共用同一資料庫,導致資料污染。 | 建立 多個 .env.* 檔,並在部署腳本中指定正確檔案。 |
| 忘記在 Docker / PM2 等容器中設定環境變數 | 本地測試正常,部署後崩潰。 | 在 Dockerfile、docker-compose.yml、PM2 ecosystem file 中明確宣告 environment。 |
最佳實踐清單
- 型別宣告 + 封裝:
env.d.ts+config.ts。 - 使用
dotenv:只在開發或測試環境載入.env,正式環境使用平台變數。 - 預設值與驗證:所有必填變數必須在啟動前驗證,缺失即拋錯。
- 保密資訊絕不寫入程式碼:金鑰、密碼、憑證等只能放在環境變數或 Secret 管理系統。
- 版本控制:
.env.example放入版本庫,說明必填欄位與範例值;.env*本身加入.gitignore。 - 自動化測試:在 CI 中使用
dotenv-cli或直接在測試腳本前export NODE_ENV=test,確保測試環境的變數正確。
實際應用場景
1. 建立可配置的 API 伺服器
// src/app.ts
import express, { Request, Response } from 'express';
import { env } from './config';
const app = express();
app.get('/status', (_req: Request, res: Response) => {
res.json({
env: env.nodeEnv,
version: process.env.npm_package_version,
uptime: process.uptime(),
});
});
app.listen(env.port, () => {
console.log(`📡 API Server listening on ${env.port}`);
});
使用情境:
- 開發:
NODE_ENV=development、PORT=4000。 - 測試:
NODE_ENV=test、PORT=5000(測試用)。 - 正式:
NODE_ENV=production、PORT=80,同時透過DATABASE_URL連接雲端資料庫。
2. 與第三方服務(如 Stripe)整合
// src/payment/stripe.ts
import Stripe from 'stripe';
import { env } from '../config';
if (!env.apiKey) {
throw new Error('❌ Stripe API key 未設定(API_KEY)');
}
export const stripe = new Stripe(env.apiKey, {
apiVersion: '2023-10-05',
});
說明:金鑰只在環境變數中保存,避免在 Git 中洩漏;部署時可在雲端平台(Heroku、Vercel)設定 API_KEY。
3. Docker + Node.js + TypeScript 的完整範例
Dockerfile
# 建立階段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build # tsc 產出 dist
# 執行階段
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --production
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
env_file:
- .env.production # 只在正式環境使用
restart: unless-stopped
重點:Docker 只在 env_file 中注入環境變數,程式碼本身仍透過 config.ts 取得並驗證。
總結
process.env是 Node.js 提供的全域環境變數容器,但在 TypeScript 中若不加以型別宣告與驗證,會失去靜態檢查的好處。- 透過 自訂型別宣告 (
env.d.ts)、封裝讀取與驗證 (config.ts),我們可以在開發階段即捕捉缺失或格式錯誤,提升程式的可靠性。 - 使用
dotenv方便在本機或測試環境快速載入.env檔;在正式環境則建議直接使用平台提供的環境變數或 Secret 管理服務。 - 最佳實踐 包括:型別安全、預設值與驗證、
.gitignore隱藏機密、分環境.env檔、Docker/CI 中明確設定。 - 實務上,環境變數不只用於 Port、Database URL,還涵蓋 API 金鑰、第三方服務憑證、Feature Flag 等,正確的管理方式能讓專案在 開發 → 測試 → 部署 的全流程中保持一致且安全。
掌握以上概念與技巧,你就能在 Node.js + TypeScript 專案中,像使用其他 TypeScript 型別一樣安全、方便地使用環境變數,讓應用程式在不同執行環境中快速切換、易於維護。祝你開發順利!