TypeScript でコードを書いた際、ソースのテストは通っていても、何かの弾みでビルドされたコードが壊れていないことを念のために確認しておきたいという需要はある。とくにライブラリを作っていて、npm に公開する場合など、publish 前に検証するのは良い考えだろう。

このために、同じテストコードをソースと生成物それぞれに対して適用する方法を考える。つまり、以下のような conditional import を行いたい。

// このコードは動かない。

if (process.env.TESTMODE === 'dist') {
  import { FooClass, FooString } from '../dist/Foo';
}
else
  import { FooClass, FooString } from '../src/Foo';
}

describe('Test foo', function() {
  it('works', function() {
    const fooString: FooString = 'foo'; // このように型やインターフェースも使いたい
    assert(FooClass.getFooString() === fooString);
  });
});

TypeScript の import ... from 文は、名前空間やモジュールの直下でなければ使えないため、上記のように単純にはいかないし、インポート元は文字列リテラルでなければいけないので、import { FooClass } from `../${getTestDir()}/Target` とすることもできない。

動的な require は可能なので、const { FooClass } = require(`../${getTestDir()}/Foo`); とすることはできるが、型情報が失われるのでテストを書く際に型による支援を受けられづらくなる。

とはいえ、今回のケースに限れば、読み込み元が変わったとしても、型は変わらないので、以下のようにして型情報をソースから流用することで解決する。FooClassの型情報を使うためだけにソースから FooClass_FooClass としてインポートし、実体が require される先の変数の型として、typeof _FooClass としている。インターフェースはソースから直接読み込むしかない。

import { FooClass as _FooClass, FooString } from '../src/Foo';

let FooClass: typeof _FooClass;
if (process.env.TESTMODE === 'dist') {
  FooClass = require('../dist/Foo').FooClass;
}
else {
  FooClass = require('../src/Foo').FooClass;
}

describe('Test foo', function() {
  it('works', function() {
    const fooString: FooString = 'foo'; // このように型やインターフェースも使いたい
    assert(FooClass.getFooString() === fooString);
  });
});

あるいはテンプレート文字列を使うなら以下のようになるだろう。

import { FooClass as _FooClass, FooString } from '../src/Foo';

// getTestDir() は環境変数を見て適当な読み込み元ディレクトリを返す関数とする。
const FooClass: typeof _FooClass= require(`../${getTestDir()}/Foo`).FooClass;

以上のようにしておくと、package.json

"scripts": {
  "test": "mocha --require espower-typescript/guess test/**/*.ts",
  "disttest": "TESTMODE=dist npm run test",
  "prepublishOnly": "npm test && npm run disttest"
}

としておくことで、npm test でソースのテストを、npm run disttest でビルド生成物のテストを実行できる。また publish 前には prepublishOnly によって自動で両方のテストが走り、動かないパッケージを公開してしまうことを避けられる。

なお、TypeScript 2.4 で導入された Dynamic Import を使う方法もあるが、モジュールが非同期で読み込まれるため Promise の解決をしなければならない。テストのために雑に読み込みたい状況であれば、単純に require したほうが楽だろう。