その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

TypeScriptを使ってアプリケーションの設定ファイルを書く

アプリケーションの設定ファイルと言えばjsonyamlなんかで書くことが多いと思うんですが,最近はTypeScriptで設定ファイルを書いております.このような感じ:

export interface ApplicationConfig {
  readonly listenPort: number;
  readonly timeout: number;
  readonly apiBaseUrl: string;
  readonly loggerOptions: LoggerOptions;
}

こういう感じで設定のinterfaceを定義しておいて

export class ProductionConfig implements ApplicationConfig {
  readonly listenPort = 5963;
  readonly timeout = 5000;
  readonly apiBaseUrl = "http://internal-api.example.com";
  readonly loggerOptions = {
    level: 'info',
  };
}

// TypeScript (JavaScript) 内で使うなら以下のようにしておくと便利そう (例A)
const productionConfig = new ProductionConfig();
export { productionConfig };

というふうに実際の設定を記述するという感じです.


メリットとしては,

  • 「何が設定項目なのか」が定義により明らかになる
  • 設定漏れが基本的に無い
    • これはinterfaceのサポートによるもの
  • 型のサポートがある
    • 設定の構造化ができる
  • 継承が使える
    • というかTypeScriptの言語機能を使ったコードが書ける

といったあたりが挙げられると思います.

interfaceの定義によって,「どういった項目が設定されるべきなのか」が「型情報も含めて」明示できるというのは利点の一つと言って良いと思います.

また,ウェブアプリケーションだと例えば本番環境用の設定,ステージング環境用の設定,開発環境用の設定……というふうに複数の設定が存在していることがままあると思うのですが,それぞれの環境のためにinterfaceを満足した設定を記述 (実装) してあげるだけで,漏れが無い (つまりそれぞれが等価な振る舞いをする) 設定を安全に用意することができるようになります.
当然のことながら,設定の実装クラスがinterfaceを実装しきらなかった場合はコンパイルエラーが出るので堅牢です.

型のサポートが得られるというのは言うまでもないですね.安全! 型定義の無い設定を取り扱う際に苦労することがままありましたからね……
あと自分で定義したclassについてもpropertyの型にすることができるので便利です.

個人的には継承が使えるというのは便利ポイントの一つだと思っていまして,例えば「基本的な動作はまったく同じなんだけど一部の機能だけ有効 (あるいは無効) にしたい」みたいなことがあったときに継承は便利に使えるかな〜と思っています.例えばこのような感じ:

export interface ApplicationConfig {
  readonly port: number;
  readonly tcpdumpEnabled: boolean;
}

export class ProductionConfig implements ApplicationConfig {
  readonly port = 5963;
  constructor(readonly tcpdumpEnabled: boolean = false) {
  }
}

export class ProductionInspectorConfig extends ProductionConfig{
  constructor() {
    super(true);
  }
}

このようにしておくと,基本的には ProductionConfig と基本的には同じ設定でありながらも, tcpdumpEnabledtrue にした ProductionInspectorConfig をお手軽に作れることとなります.
もちろん通常のソフトウェアにおける継承と同じで,is-aとして取り扱える (に等しい) 状況以外で使うと良くないことが起きると思うしそもそも継承を使うと複雑さが容易に増すので,そこは用法用量を守って……という感じでしょう.というかこれはTypeScriptの言語機能を設定記述に使えるという話に過ぎませんね.


あとはTypeScript (JavaScript) 内で使うのならば個別の設定のインスタンスをexportしてしまうか (上記の例A),あるいはinterface側に例えば以下のようなコードを書いて,それによって得られたインスタンスをexportしてしまえば便利そうです:

export interface ApplicationConfig {
  ...
}

const appConfig: ApplicationConfig = (() => {
  const nodeEnv = process.env.NODE_ENV;
  switch (nodeEnv) {
    case 'local':
      return new LocalConfig();
    case 'staging':
      return new StagingConfig();
    case 'production':
      return new ProductionConfig();
    default:
      throw new Error('invalid NODE_ENV: ' + nodeEnv);
  }
})();
export { appConfig };

また,TypeScript以外でこの設定を利用したい,つまり単なる設定記述言語としてこれを利用したい場合は生成した設定インスタンスJSON.stringify()に食わせてしまえば他の処理系でも取り扱えるJSON形式の設定が得られるのでそのようなスクリプトを書くとポータブル.良いですね.良かった良かった.


と,このような感じでした.TypeScriptは書き味が比較的ライトなので,設定ファイル用途にも書きやすくて良いですね.