技術アーカイブス
Next.jsの環境変数にTypescriptの型を定義する方法

環境変数を使用する際、毎回定義があるかの確認や数値への変換、IDEのコード補完が使えないなどの面倒な点が多数あります。 そこで、事前に環境変数の型を定義することで、コードの品質向上とコーディング時の利便性を改善する方法を、当社では採用しています。
環境変数ファイル .env.production に環境変数を設定します。
NEXT_VAR="テスト"
EXSAMPLE_VAR_STRING="文字列"
EXSAMPLE_VAR_NUMBER=1234
次に env.d.ts ファイルを作成し、環境変数の型を定義します。
export type CustomProcessEnv = {
NEXT_VAR: string
EXSAMPLE_VAR_STRING: string
EXSAMPLE_VAR_NUMBER: number
}
declare namespace NodeJS {
interface ProcessEnv extends CustomProcessEnv {}
}
ビルド時に env.d.ts ファイルを読み込み、環境変数と型をマッピングしたファイルを生成するためのスクリプト generate_env_map.js ファイルを作成します。 ファイルの生成はts-morphというTypescriptのコードを解析、リファクタリング、生成などの処理を行うライブラリを利用します。 また、環境変数ファイルなどを読み込みためのdotenvも利用しますので、インストールされていなければ追加してください。
const { Project } = require('ts-morph')
const fs = require('fs')
const path = require('path')
// .envを読み込む
require('dotenv').config()
/**
* 環境変数の型マッピングを生成します。
*/
function generateEnvMap() {
const envDtsPath = path.resolve(__dirname, 'env.d.ts')
const envMapPath = path.resolve(__dirname, 'env_map.ts')
// プロジェクトの設定
const project = new Project({
tsConfigFilePath: path.resolve(__dirname, 'tsconfig.json'),
})
// 型定義ファイルを追加
const sourceFile = project.addSourceFileAtPath(envDtsPath)
// 型エイリアスを取得
const typeAlias = sourceFile.getTypeAliasOrThrow('CustomProcessEnv')
// プロパティを取得
const properties = typeAlias.getType().getProperties()
// 環境変数のマッピングを収集
const envVars = properties.map((prop) => {
const name = prop.getName()
const declarations = prop.getDeclarations()
if (declarations.length === 0) {
throw new Error(`No declarations found for environment variable ${name}`)
}
const type = declarations[0].getType().getText()
if (type !== 'string' && type !== 'number' && type !== 'boolean') {
throw new Error(`Unsupported type for environment variable ${name}: ${type}`)
}
return { name, type }
})
// サーバーサイドとクライアントサイドのマッピングを分離
const serverEnvVars = envVars
const clientEnvVars = envVars.filter((env) => env.name.startsWith('NEXT_PUBLIC_'))
// クライアントサイド用の値を取得
const clientEnvValues = clientEnvVars.map((env) => {
const value = process.env[env.name]
if (value === undefined) {
throw new Error(`Environment variable ${env.name} is not defined.`)
}
// 型に基づいて値を適切に変換
let formattedValue
switch (env.type) {
case 'boolean':
formattedValue = value === 'true'
break
case 'number':
const num = Number(value)
if (isNaN(num)) {
throw new Error(`Environment variable ${env.name} should be a valid number.`)
}
formattedValue = num
break
case 'string':
default:
formattedValue = value
}
return { name: env.name, type: env.type, value: formattedValue }
})
// マッピングファイルのテンプレート
const fileContent = `
export const serverEnvMap: Record<string, 'string' | 'number' | 'boolean'> = {
${serverEnvVars.map((env) => `'${env.name}': '${env.type}',`).join('\n ')}
};
export const clientEnvMap: Record<string, { type: 'string' | 'number' | 'boolean'; value: string | number | boolean }> = {
${clientEnvValues.map((env) => `'${env.name}': { type: '${env.type}', value: ${JSON.stringify(env.value)} },`).join('\n ')}
};
`
// ファイルを書き出す
fs.writeFileSync(envMapPath, fileContent, { encoding: 'utf8' })
console.log('env_map.ts has been generated successfully.')
}
module.exports = { generateEnvMap }
// スクリプトが直接実行された場合に関数を呼び出す
if (require.main === module) {
generateEnvMap()
}
buildやstart時に env_map.ts ファイルが生成されるように、以下のように package.json を修正します。
{
"name": "Youre App",
"version": "0.0.1",
"license": "",
"scripts": {
"generate_env_map": "dotenvx run -f .env.production -- node generate_env_map.js",
"dev": "pnpm generate_env_map && NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --turbo",
"build": "pnpm generate_env_map && next build",
"start": "pnpm generate_env_map && next start",
},
...
生成されたファイル env_map.ts は以下のように出力されます。
export const serverEnvMap: Record<string, 'string' | 'number' | 'boolean'> = {
'NEXT_VAR': string
'EXSAMPLE_VAR_STRING': 'string'
'EXSAMPLE_VAR_NUMBER': 'number'
};
export const clientEnvMap: Record<string, { type: 'string' | 'number' | 'boolean'; value: string | number | boolean }> = {
'NEXT_VAR': { type: 'string', value: "テスト" },
};
環境変数を利用するためのユーティリティ EnvUtil.ts を作成します。
import { CustomProcessEnv } from 'env'
import { serverEnvMap, clientEnvMap } from 'env_map'
/**
* 環境変数を取得します。
*
* @param key - 環境変数のキー
* @param options - { type?: 'string' | 'number' | 'boolean', default?: CustomProcessEnv[K] }
* @returns 環境変数の値(存在しない場合は default)
*/
export function getEnv<K extends keyof CustomProcessEnv>(key: K, defaultValue?: CustomProcessEnv[K]): CustomProcessEnv[K] {
if (defaultValue !== undefined) {
return defaultValue
}
let type: 'string' | 'number' | 'boolean' = 'string'
let value: string | number | boolean = ''
// サーバーサイド
if (typeof window === 'undefined') {
type = serverEnvMap[key]
if (!type) {
throw new Error(`${key} 環境変数は定義されていません。`)
}
value = process.env[key] as string
if (value === undefined) {
throw new Error(`${key} 環境変数を定義してください。`)
}
} else {
// クライアントサイド
const env = clientEnvMap[key]
if (!env) {
throw new Error(`${key} 環境変数を定義してください。`)
}
type = env.type
value = env.value
}
switch (type) {
case 'boolean':
return ((typeof value === 'boolean' && value) || value === 'true') as CustomProcessEnv[K]
case 'number':
const parsedNumber = Number(value)
if (isNaN(parsedNumber)) {
throw new Error(`${key} 環境変数は数値で設定してください。[VALUE] ${value}`)
}
return parsedNumber as CustomProcessEnv[K]
case 'string':
default:
return value as CustomProcessEnv[K]
}
}
以下のようにgetEnv関数を使い環境変数を使うことができます。コード補完も出来るようになりますので試してみてください。
import { getEnv } from 'EnvUtil'
getEnv('NEXT_VAR')