TypeScript で funfix を使用 - tsc, FuseBox

funfixJavaScript, TypeScript, Flow の関数型プログラミング用ライブラリで、Fantasy LandStatic Land ※ に準拠し Scala の Option, Either, Try, Future 等と同等の型が用意されているようです。

 ※ JavaScript 用に Monoid や Monad 等の代数的構造に関する仕様を定義したもの

今回は Option を使った単純な処理を TypeScript で実装し Node.js で実行してみます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20180730/

はじめに

Option を使った下記サンプルをコンパイルして実行します。

サンプルソース
import { Option, Some } from 'funfix'

const f = (ma, mb) => ma.flatMap(a => mb.map(b => `${a} + ${b} = ${a + b}`))

const d1 = Some(10)
const d2 = Some(2)

console.log( d1 )

console.log('-----')

console.log( f(d1, d2) )
console.log( f(d1, Option.none()) )

console.log('-----')

console.log( f(d1, d2).getOrElse('none') )
console.log( f(d1, Option.none()).getOrElse('none') )

ビルドと実行

上記ソースファイルを以下の 2通りでビルドして実行してみます。

  • (a) tsc 利用
  • (b) FuseBox 利用

(a) tsc を利用する場合

tsc コマンドを使って TypeScript のソースをコンパイルします。

まずは typescript と funfix モジュールをそれぞれインストールします。

typescript インストール
> npm install --save-dev typescript
funfix インストール
> npm install --save funfix

この状態で sample.ts ファイルをコンパイルしてみると、型関係のエラーが出るものの sample.js は正常に作られました。

コンパイル1
> tsc sample.ts

node_modules/funfix-core/dist/disjunctions.d.ts:775:14 - error TS2416: Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'.
  Type 'undefined' is not assignable to type 'never'.

775     readonly value: undefined;
                 ~~~~~


node_modules/funfix-effect/dist/eval.d.ts:256:42 - error TS2304: Cannot find name 'Iterable'.

256     static sequence<A>(list: Eval<A>[] | Iterable<Eval<A>>): Eval<A[]>;
                                             ~~~~~~~~
・・・

sample.js を実行してみると特に問題無く動作します。

実行1
> node sample.js

TSome { _isEmpty: false, value: 10 }
-----
TSome { _isEmpty: false, value: '10 + 2 = 12' }
TNone { _isEmpty: true, value: undefined }
-----
10 + 2 = 12
none

これで一応は動いたわけですが、コンパイル時にエラーが出るというのも望ましい状態ではないので、エラー部分を解決してみます。

他にも方法があるかもしれませんが、ここでは以下のように対応します。

  • (1) Property 'value' in type 'TNone' ・・・ 'Option<never>' のエラーに対して tsc 実行時に --strictNullChecks オプションを指定して対応
  • (2) Cannot find name 'Iterable' 等のエラーに対して @types/node をインストールして対応

strictNullChecks は tsc の実行時オプションで指定する以外にも設定ファイル tsconfig.json で設定する事もできるので、ここでは tsconfig.json ファイルを使います。

(1) tsconfig.json
{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

次に @types/node をインストールします。

@types/node には Node.js で実行するための型定義(Node.js 依存の API 等)が TypeScript 用に定義されています。

(2) @types/node インストール
> npm install --save-dev @types/node

この状態で tsc を実行すると先程のようなエラーは出なくなりました。(tsconfig.json を適用するため tsc コマンドへ引数を指定していない点に注意)

コンパイル2
> tsc

実行結果にも差異はありません。

実行2
> node sample.js

TSome { _isEmpty: false, value: 10 }
-----
TSome { _isEmpty: false, value: '10 + 2 = 12' }
TNone { _isEmpty: true, value: undefined }
-----
10 + 2 = 12
none

最終的な package.json の内容は以下の通りです。

package.json
{
  "name": "sample",
  "version": "1.0.0",
  "devDependencies": {
    "@types/node": "^10.5.4",
    "typescript": "^2.9.2"
  },
  "dependencies": {
    "funfix": "^7.0.1"
  }
}

(b) FuseBox を利用する場合

次に、モジュールバンドラーの FuseBox を使用してみます。(以降は (a) とは異なるディレクトリで実施)

なお、ここでは npm の代わりに yarn を使っていますが、npm でも特に問題はありません。

yarn のインストール例(npm 使用)
> npm install -g yarn

(b-1) 型チェック無し

まずは typescript, fuse-box, funfix をそれぞれインストールしておきます。

typescript と fuse-box インストール
> yarn add --dev typescript fuse-box
funfix インストール
> yarn add funfix

FuseBox ではビルド定義を JavaScript のコードで記載します。 とりあえずは必要最小限の設定を行いました。

bundle で指定した名称が init の $name に適用されるため、*.tsコンパイル結果と依存モジュールの内容をバンドルして bundle.js へ出力する事になります。

なお、> でロード時に実行する(コードを記載した)ファイルを指定します。

fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box')

const fuse = FuseBox.init({
    output: '$name.js'
})

fuse.bundle('bundle').instructions('> *.ts')

fuse.run()

上記スクリプトを実行してビルド(TypeScript のコンパイルとバンドル)を行います。

ビルド
> node fuse.js

--- FuseBox 3.4.0 ---
  → Generating recommended tsconfig.json:  ・・・\sample_fusebox1\tsconfig.json
  → Typescript script target: ES7

--------------------------
Bundle "bundle"

    sample.js
└──  (1 files,  700 Bytes) default
└── funfix-core 34.4 kB (1 files)
└── funfix-effect 43.1 kB (1 files)
└── funfix-exec 79.5 kB (1 files)
└── funfix 1 kB (1 files)
size: 158.7 kB in 765ms

初回実行時にデフォルト設定の tsconfig.json が作られました。(tsconfig.json が存在しない場合)

tsc の時のような型関係のエラーは出ていませんが、これは FuseBox がデフォルトで TypeScript の型チェックをしていない事が原因のようです。

型チェックを実施するには fuse-box-typechecker プラグインを使う必要がありそうです。

実行
> node bundle.js

TSome { _isEmpty: false, value: 10 }
-----
TSome { _isEmpty: false, value: '10 + 2 = 12' }
TNone { _isEmpty: true, value: undefined }
-----
10 + 2 = 12
none

package.json の内容は以下の通りです。

package.json
{
  "name": "sample_fusebox1",
  "version": "1.0.0",
  "main": "bundle.js",
  "license": "MIT",
  "devDependencies": {
    "fuse-box": "^3.4.0",
    "typescript": "^2.9.2"
  },
  "dependencies": {
    "funfix": "^7.0.1"
  }
}

(b-2) 型チェック有り

TypeScript の型チェックを行うようにしてみます。

まずは、(b-1) と同じ構成に fuse-box-typechecker プラグインを加えます。

fuse-box-typechecker を追加インストール
> yarn add --dev fuse-box-typechecker

次に、fuse.js へ fuse-box-typechecker プラグインの設定を追加します。

TypeChecker で型チェックにエラーがあった場合、例外が throw されるようにはなっていないため、ここではエラーがあった場合に Error を throw して fuse.run() を実行しないようにしてみました。

ただし、こうすると tsconfig.json を予め用意しておく必要があります。(TypeChecker に tsconfig.json が必要)

fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box')
const {TypeChecker} = require('fuse-box-typechecker')

// fuse-box-typechecker の設定
const testSync = TypeChecker({
    tsConfig: './tsconfig.json'
})

const fuse = FuseBox.init({
    output: '$name.js'
})

fuse.bundle('bundle').instructions('> *.ts')

testSync.runPromise()
    .then(n => {
        if (n != 0) {
            // 型チェックでエラーがあった場合
            throw new Error(n)
        }
        // 型チェックでエラーがなかった場合
        return fuse.run()
    })
    .catch(console.error)

これで、ビルド時に (a) と同様の型エラーが出るようになりました。

ビルド1
> node fuse.js

・・・
--- FuseBox 3.4.0 ---

Typechecker plugin(promisesync) .
Time:Sun Jul 29 2018 12:40:47 GMT+0900 (GMT+09:00)

File errors:
└── .\node_modules\funfix-core\dist\disjunctions.d.ts
   | ・・・\sample_fusebox2\node_modules\funfix-core\dist\disjunctions.d.ts (775,14) (Error:TS2416) Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'.
  Type 'undefined' is not assignable to type 'never'.

Errors:1
└── Options: 0
└── Global: 0
└── Syntactic: 0
└── Semantic: 1
└── TsLint: 0

Typechecking time: 4116ms
Quitting typechecker

・・・

ここで、Iterable の型エラーが出ていないのは fuse-box-typechecker のインストール時に @types/node もインストールされているためです。

(a) と同様に strictNullChecks の設定を tsconfig.json追記して、このエラーを解決します。

tsconfig.json へ strictNullChecks の設定を追加
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES7",
    ・・・
    "strictNullChecks": true
  }
}

これでビルドが成功するようになりました。

ビルド2
> node fuse.js

・・・
Typechecker name: undefined
Typechecker basepath: ・・・\sample_fusebox2
Typechecker tsconfig: ・・・\sample_fusebox2\tsconfig.json
--- FuseBox 3.4.0 ---

Typechecker plugin(promisesync) .
Time:Sun Jul 29 2018 12:44:57 GMT+0900 (GMT+09:00)
All good, no errors :-)
Typechecking time: 4103ms
Quitting typechecker

killing worker  → Typescript config file:  \tsconfig.json
  → Typescript script target: ES7

--------------------------
Bundle "bundle"

    sample.js
└──  (1 files,  700 Bytes) default
└── funfix-core 34.4 kB (1 files)
└── funfix-effect 43.1 kB (1 files)
└── funfix-exec 79.5 kB (1 files)
└── funfix 1 kB (1 files)
size: 158.7 kB in 664ms
実行結果
> node bundle.js

TSome { _isEmpty: false, value: 10 }
-----
TSome { _isEmpty: false, value: '10 + 2 = 12' }
TNone { _isEmpty: true, value: undefined }
-----
10 + 2 = 12
none

package.json の内容は以下の通りです。

package.json
{
  "name": "sample_fusebox2",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "fuse-box": "^3.4.0",
    "fuse-box-typechecker": "^2.10.0",
    "typescript": "^2.9.2"
  },
  "dependencies": {
    "funfix": "^7.0.1"
  }
}