efcl.info Open in urlscan Pro
188.114.96.3  Public Scan

URL: https://efcl.info/2024/08/24/type-safe-env/
Submission: On October 04 via manual from JP — Scanned from NL

Form analysis 1 forms found in the DOM

GET https://www.google.co.jp/search

<form class="search-box" method="get" action="https://www.google.co.jp/search">
  <label for="search-box-label">検索:</label>
  <input type="hidden" name="q" value="site:efcl.info">
  <input id="search-box-label" type="search" name="q">
  <input type="submit" value="search">
</form>

Text Content

WEB SCRATCH

ブラウザ/JavaScript等についてのブログ


AZU


検索:


最近の投稿

 * JavaScript Primer v6.0.0リリース: ES2024の対応とNode.jsのユースケースを刷新
 * Node.jsで型安全な環境変数を扱うスニペット
 * モバイル端末でのウェブアプリのデバッグ方法、Safari on iOS/Chrome on Android
 * JavaScript PrimerのES2024対応を手伝ってくれるContributorとSponsorを募集しています


カテゴリ一覧

 * JavaScript[213]
 * イベント[107]
 * 雑記[89]
   その他のタグ…


NODE.JSで型安全な環境変数を扱うスニペット

2024年08月24日 Edit on GitHub

Node.js で型安全な環境変数を扱うスニペットを作りました。

next devのようなアプリケーションの起動、Playwright でのテストなどコマンドごとに渡したい環境変数のセットが異なるケースがあります。
この場合に環境変数をまとめたものを定義して、それをコマンドごとに読み込むセットを変えたいことがあります。

次のようにベタ書きしてもいいのですが、渡したい環境変数が増えると管理が大変になります。

NEXT_PUBLIC_LOCALHOST_URL=http://localhost:3000 NEXT_PUBLIC_API_URL=http://localhost:3001 NEXT_PUBLIC_IS_TEST_MODE=false FOO="bar" next dev


そのため、.envのような環境変数をまとめたファイルを使いたくなります。 Node.js
は--env-fileフラグで.envファイルを読み込むことができますが、.envファイルは型安全ではありません。

 * --env-file
 * Node.js — How to read environment variables from Node.js

環境変数名の Typoや必須の環境変数が設定されていないなどの問題が発生する可能性があります。 そのため環境変数をの定義自体も TypeScript
で型安全に定義したいです。

これをやるための 50 行ほどのスニペットを書いたので、使い方を紹介します。

 * 📝 主な用途はローカルやCIでの開発用で、実際にデプロイするサーバなどには利用しない想定です
 * 📝 ライブラリとかにしてないのは、この仕組み自体が外部パッケージの依存もなく短いスニペットだからです
   * JSからTSを参照する都合上、ライブラリにはちょっとしにくい気がします


サンプルリポジトリ

次の場所にサンプルリポジトリがあります。

 * azu/type-safe-env: Type Safe /Environment Variables snippet for Node.js


使い方

大きく分けて、環境変数の型を定義するdefineEnv関数と、環境変数をセットするsetEnv関数があります。


環境変数の型を定義する

 * defineEnv.ts: 環境変数の型を定義する Utility

defineEnv関数を次のような受け取りたい環境変数の型定義をするUtilityです。

export type BaseEnvRecord = Record<
    string,
    {
        value: string | undefined;
        required: boolean;
        defaultValue?: string;
    }
>;
export type ReturnTypeOfCreateEnv<T extends BaseEnvRecord> = {
    // If the value is required, it should be a string, otherwise it should be a string or undefined
    [K in keyof T]: T[K]["required"] extends true ? string : string | undefined;
};
/**
 * Define environment variables and create them
 */
export const defineEnv = <T extends BaseEnvRecord>(envs: T): ReturnTypeOfCreateEnv<T> => {
    const result = new Map<string, string | undefined>();
    Object.entries(envs).forEach(([key, { value, required, defaultValue }]) => {
        if (required && !value && !defaultValue) {
            throw new Error(
                `Missing required environment variable: ${key}, value: ${value === undefined ? "undefined" : `"${value}"`}`
            );
        }
        result.set(key, value || defaultValue);
    });
    return Object.fromEntries(result) as ReturnTypeOfCreateEnv<T>;
};


 * env.ts: アプリケーション用の環境変数を定義する

env.tsでは、defineEnv関数を使ってアプリケーションで受け取りたい環境変数の型を定義します。

import { defineEnv } from "./defineEnv";

export const env = defineEnv({
  /**
   * Localhost URL
   */
  LOCALHOST_URL: {
    value: process.env["LOCALHOST_URL"],
    required: true,
    defaultValue: "http://localhost:3000",
  },
  /**
   * Is test mode?
   */
  IS_TEST_MODE: {
    value: process.env["IS_TEST_MODE"],
    required: true,
    defaultValue: "false",
  },
  /**
   * Optional value
   */
  OPTIONAL_VALUE: {
    value: process.env["OPTIONAL_VALUE"],
    required: false,
  },
});


このときに、Node.js ならprocess.envを使って環境変数を取得します。
値ごとにrequiredで必須かどうかの指定や、defaultValueでデフォルト値を指定できます。 具体的には、required: true で
defaultValueが指定されていなくて、process.env.*の値がない場合はエラーになります。

この defineEnv関数で定義したenvはアプリケーションが使う環境変数をまとめたものです。

次のようにアプリケーションからはenvを import して使います。 defineEnv関数で定義した環境変数を型安全に使うことができます。

// use env
import { env } from "../env/env";
// type-safe
console.log("localhost url", env.LOCALHOST_URL);
console.log("Is Test Mode", env.IS_TEST_MODE);
console.log("OPTIONAL_VALUE", env.OPTIONAL_VALUE); // string or undefined


一方で、process.envに設定する環境変数自体も型安全に設定したいです。


環境変数をセットする

次のsetEnv関数を使って、プロセスに対して環境変数をセットします。

 * setEnv.js: 環境変数をセットする Utility

例として、次のようなenv.local.jsとenv.test.jsのような環境変数をまとめたファイルを作ります。

 * env.local.js: ローカル開発用の環境変数をセットする

import { setEnv } from "./src/env/setEnv";

// local環境用の環境変数をセット
setEnv({
  LOCALHOST_URL: "http://localhost:3500",
  IS_TEST_MODE: "false",
});


 * env.test.js: テスト用の環境変数をセットする

import { setEnv } from "./src/env/setEnv";

// test環境用の環境変数をセット
setEnv({
  LOCALHOST_URL: "http://localhost:3500",
  IS_TEST_MODE: "true",
});


あとは、このenv.*.jsをNODE_OPTIONSを使って読み込むことで、環境変数をセットできます。

例えば、env.local.jsを使って開発サーバーを起動する場合は次のようにします。

NODE_OPTIONS='--import ./env.local.js' npm run dev


テストを実行する場合は、env.test.jsを使って次のようにします。

NODE_OPTIONS='--import ./env.test.js' npm test


これで、npm run devやnpm testなどのコマンドごとに異なる環境変数をセットできるようになります。


仕組み

defineEnv.tsの方はただのTypeScriptなのであまり問題ないと思います。

setEnv.jsの方は、TypeScriptではなくJavaScriptで書いていますが、checkJsを使って型チェックを行っています。

具体的には、次のようなtsconfig.jsonを使って、env.*.jsを型チェックしています。
allowJsを有効化していますが、通常はなんでも.jsをtscでは扱いたくないので、env.*.jsだけをincludesに指定しています

{
  "compilerOptions": {
    // ....
    // Type Check for env.*.js
    "allowJs": true,
    "checkJs": true
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    // allow to check js files
    "./src/env/setEnv.js",
    "env.*.js"
  ],
  "exclude": [
    ".git",
    "node_modules"
  ]
}


setEnv.jsの方は、JSDocを使って受け取れる環境変数の型を定義しているのでcheckJsが有効になっていると型チェックが行われます。

import process from "node:process";

/**
 * set env util
 * @param {Partial<typeof import("./env").env>} env
 */
export const setEnv = (env) => {
    if (process.env["PRINT_ENV"] === "true") {
        console.table(env);
    }
    Object.entries(env).forEach(([key, value]) => {
        process.env[key] = value;
    });
};



FAQ

.ENVの代わりにENV.*.JSを使う理由

.envファイルは型安全ではないです。 env.*.jsはTypeScriptのcheckJs機能で型チェックされるため型安全です。

Node.js は--env-fileフラグで.envファイルを読み込むことができます。

 * --env-file
 * Node.js — How to read environment variables from Node.js

しかし、NODE_OPTIONS="--env-file=.env"は許可されていません。

 * Node 20.6+ --env-file flag in NODE_OPTIONS is not allowed · Issue #1096 ·
   cypress-io/github-action

ENV.*.TSの代わりにENV.*.JSを使う理由

Node.js の--experimental-strip-typesはまだ実験的な機能です。

ts-nodeやtsxなどを使えば.tsでも書けますが、あえてTypeScriptの変換を入れるほどでもないのでenv.*.jsを使っています。

直接--IMPORTフラグを使わずにNODE_OPTIONSを使う理由

pnpmのようなパッケージマネージャはパッケージのbinをシェルスクリプトとしてインストールします。

例えば、node_modules/.bin/viteはシェルスクリプトとしてインストールされます。

そのため、nodeコマンドを使ってviteコマンドを実行できません。

node --import=./env.local.js node_modules/.bin/vite
# エラーになる


NODE_OPTIONS=optionsを使うことで、Node.jsプロセスにオプションを渡すことができます。

NODE_OPTIONS='--import ./env.local.js' node_modules/.bin/vite
# これなら動く



まとめ

Node.js で型安全な環境変数を扱うスニペットを作りました。

 * defineEnv関数で環境変数の型を定義
 * setEnv関数で環境変数をセット
 * NODE_OPTIONSでenv.*.jsを読み込むことで、コマンドごとに異なる環境変数をセット

50行ほどのスニペットですが、環境変数を型安全に扱うことができるので結構便利でした。


参考

 * azu/type-safe-env: Type Safe /Environment Variables snippet for Node.js

修正リクエストをする
タグ:
 * JavaScript
 * Node.js


お知らせ欄

JavaScript Primerの書籍版がAmazonで購入できます。

JavaScriptに関する最新情報は週一でJSer.infoを更新しています。

GitHub Sponsorsでの支援を募集しています。


関連記事

 * JavaScript Primer v6.0.0リリース: ES2024の対応とNode.jsのユースケースを刷新
 * JavaScript PrimerのES2024対応を手伝ってくれるContributorとSponsorを募集しています
 * Twitter/Blueskyの自己ポストの全文検索サービスをNext.js App Router(RSC)で書きなおした方法/設計/感想
 * 私のJavaScriptの情報収集法 2024年版
 * #jsprimer week: 2024-02-05 - 2024-02-11

コメントを表示