Deno から npm パッケージを使用する(Deno で fp-ts)

下記の方法を用いて Node.js / ブラウザ用 npm パッケージを Deno から利用してみました。

npm パッケージは関数プログラミング用 TypeScript ライブラリの fp-ts を試すことにします。

fp-ts は CommonJS と ES Modules のモジュール形式に対応していますが、現時点で Deno に対する直接的なサポートは無さそうでした。

また、使用した Deno のバージョンは以下の通りです。

  • Deno 1.3.1

今回のサンプルコードは http://github.com/fits/try_samples/tree/master/blog/20200825/

はじめに

まずは、Node.js でサンプルコードを作成してみました。

sample.ts
import { Option, some, none, map } from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'

const f = (d: Option<number>) => 
    pipe(
        d,
        map(v => v + 2),
        map(v => v * 3)
    )

console.log( f(some(5)) )
console.log( f(none) )

実行結果は以下の通りです。

sample.ts 実行結果
> npm install ts-node typescript fp-ts
・・・

> ts-node sample.ts
{ _tag: 'Some', value: 21 }
{ _tag: 'None' }

(a) Skypack の使用

前回の GraphQL.js でも利用しましたが、Skypack は npm パッケージをブラウザから直接使えるようにするための CDN となっています。

CommonJS 形式の npm パッケージを ES Modules 形式で提供する機能や Deno をサポートする機能(https://docs.skypack.dev/code/deno)が用意されているようです。

型情報なし

まずは、fp-ts の ES Modules のファイル(es6 に配置されている)を Skypack から import してみます。

TypeScript 用の型定義を指定しないと、関数の引数や戻り値などは any 型として扱う事になります。

また、import の際に拡張子を指定しなくても Skypack が .js ファイルを返してくれます。

a_1.ts
import { some, none, map } from 'https://cdn.skypack.dev/fp-ts/es6/Option.js'
// 以下でも同じ
//import { some, none, map } from 'https://cdn.skypack.dev/fp-ts/es6/Option'
import { pipe } from 'https://cdn.skypack.dev/fp-ts/es6/pipeable.js'

const f = (d: any) => 
    // @ts-ignore
    pipe(
        d,
        map( (v: number) => v + 2 ),
        map( (v: number) => v * 3 )
    )

console.log( f(some(5)) )
console.log( f(none) )
a_1.ts 実行結果
> deno run a_1.ts
・・・
{ _tag: "Some", value: 21 }
{ _tag: "None" }

ここで、@ts-ignoreVisual Studio Code におけるエラー表示対策 ※ のために付けています。

 ※ pipe 関数は、10個の any 型の引数をとる関数となっているが、
    引数を 3つしか指定していない事に対するエラー
Visual Studio Code におけるエラー表示例(@ts-ignore を付けなかった場合)

f:id:fits:20200825214530p:plain

なお、関数の引数や戻り値などを適切な型で扱うには、型定義ファイル(.d.ts)の指定が必要になりますが、fp-ts が用意している型定義ファイルを以下のように @deno-types で指定しても上手くいきません。

型定義ファイル指定の失敗例
// @deno-types="https://cdn.skypack.dev/fp-ts/es6/Option.d.ts"
import { Option, some, none, map } from 'https://cdn.skypack.dev/fp-ts/es6/Option.js'

// @deno-types="https://cdn.skypack.dev/fp-ts/es6/pipeable.d.ts"
import { pipe } from 'https://cdn.skypack.dev/fp-ts/es6/pipeable.js'

・・・

というのも、fp-ts の Option.d.tspipeable.d.ts では import { ・・・ } from './Alt' のように .d.ts の拡張子を付けずに他の型定義を import しており、不都合が生じます。※

 ※ この場合、Skypack は Alt.js を返すことになり、
    型情報を正しく取得できないと考えられる

ちなみに、Skypack 本来の使い方としては、以下のようにパッケージのルートを指定して import する事になりそうです。

a_2.ts
import { option, pipeable } from 'https://cdn.skypack.dev/fp-ts'

const { some, none, map } = option
const { pipe } = pipeable

const f = (d: any) => 
    // @ts-ignore
    pipe(
        d,
        map( (v: number) => v + 2 ),
        map( (v: number) => v * 3 )
    )

console.log( f(some(5)) )
console.log( f(none) )
a_2.ts 実行結果
> deno run a_2.ts
・・・
{ _tag: "Some", value: 21 }
{ _tag: "None" }

型定義を自作

型に関しては、型定義ファイルを自作する事で一応は解決できます。

例えば、以下のような型定義ファイルを用意します。

types/Option.d.ts
export interface None {
    readonly _tag: 'None'
}

export interface Some<A> {
    readonly _tag: 'Some'
    readonly value: A
}
export declare type Option<A> = None | Some<A>

export declare const some: <A>(a: A) => Option<A>
export declare const none: Option<never>
export declare const map: <A, B>(f: (a: A) => B) => (fa: Option<A>) => Option<B>
types/pipeable.d.ts
export declare function pipe<A, B, C, D, E, F, G, H, I, J>(
    a: A,
    ab: (a: A) => B,
    bc?: (b: B) => C,
    cd?: (c: C) => D,
    de?: (d: D) => E,
    ef?: (e: E) => F,
    fg?: (f: F) => G,
    gh?: (g: G) => H,
    hi?: (h: H) => I,
    ij?: (i: I) => J
): J

これを @deno-types で指定する事で型の問題が解決します。

a_3.ts
// @deno-types="./types/Option.d.ts"
import { Option, some, none, map } from 'https://cdn.skypack.dev/fp-ts/es6/Option.js'
// @deno-types="./types/pipeable.d.ts"
import { pipe } from 'https://cdn.skypack.dev/fp-ts/es6/pipeable.js'

const f = (d: Option<number>) => 
    pipe(
        d,
        map( v => v + 2 ),
        map( v => v * 3 )
    )

console.log( f(some(5)) )
console.log( f(none) )
a_3.ts 実行結果
> deno run a_3.ts
・・・
{ _tag: "Some", value: 21 }
{ _tag: "None" }

パッケージのルートを import する場合は、以下のような型定義ファイルを追加して @deno-types で指定します。

types/index.d.ts
import * as option from './Option.d.ts'
import * as pipeable from './pipeable.d.ts'

export {
    option,
    pipeable
}
a_4.ts
// @deno-types="./types/index.d.ts"
import { option, pipeable } from 'https://cdn.skypack.dev/fp-ts'

const { some, none, map } = option
const { pipe } = pipeable

const f = (d: option.Option<number>) => 
    pipe(
        d,
        map( v => v + 2 ),
        map( v => v * 3 )
    )

console.log( f(some(5)) )
console.log( f(none) )
a_4.ts 実行結果
> deno run a_4.ts
・・・
{ _tag: "Some", value: 21 }
{ _tag: "None" }

dts クエリパラメータの利用

Skypack には、Deno 用に型定義ファイルを解決する手段として dts クエリパラメータが用意されています。

これを使う事で、本来は以下のようなコードで型問題を解決できるはずですが、fp-ts 2.8.2 では上手くいきませんでした。

a_5e.ts
import { option, pipeable } from 'https://cdn.skypack.dev/fp-ts?dts'

const { some, none, map } = option
const { pipe } = pipeable

const f = (d: option.Option<number>) => 
    pipe(
        d,
        map( v => v + 2 ),
        map( v => v * 3 )
    )

console.log( f(some(5)) )
console.log( f(none) )

実行結果は以下のようになり、型定義ファイルの取得途中で 404 Not Found エラーが発生してしまいます。

a_5e.ts 実行結果
> deno run a_5e.ts
・・・
Download https://cdn.skypack.dev/-/fp-ts@v2.8.2-Hr9OPgW5wz4u6TqOfiZH/dist=es2020,mode=types/lib/TaskEither.d.ts
error: Import 'https://cdn.skypack.dev/-/fp-ts@v2.8.2-Hr9OPgW5wz4u6TqOfiZH/dist=es2020,mode=types/lib/HKT.d.ts' failed: 404 Not Found
Imported from "https://cdn.skypack.dev/-/fp-ts@v2.8.2-Hr9OPgW5wz4u6TqOfiZH/dist=es2020,mode=types/lib/index.d.ts:42"

これは、fp-ts の中で HKT だけ特殊な扱いがされており、HKT.d.ts ファイルが lib ディレクトリ内に配置されておらず、パッケージのルートディレクトリに配置されている事が原因だと考えられます。※

 ※ そのため、
    "/lib/HKT.d.ts" ではなく "/HKT.d.ts" を import する必要がある

    Node.js においては
    "/lib/HKT/package.json" の typings フィールドの値から
    HKT.d.ts の配置場所を取得するようになっていると思われるが、
    Skypack の dts クエリパラメータの機能では
    そこまでを考慮していない事が原因だと考えられる

ここで、dts クエリパラメータによって何が変わるのかというと、?dts を付けることでレスポンスヘッダーに x-typescript-types が付与され、型定義ファイル(.d.ts)の取得先が提示されるようになります。※

 ※ Deno は x-typescript-types ヘッダーの値から
    型定義ファイルを自動的に取得するようになっている
dts クエリパラメータの有無による違い
$ curl --head -s https://cdn.skypack.dev/fp-ts | grep x-typescript-types
$ curl --head -s https://cdn.skypack.dev/fp-ts?dts | grep x-typescript-types
x-typescript-types: /-/fp-ts@v2.8.2-Hr9OPgW5wz4u6TqOfiZH/dist=es2020,mode=types/lib/index.d.ts

更に、x-typescript-types のパスから取得できる index.d.ts は、fp-ts のオリジナル index.d.ts を(Deno 用に)加工したもの ※ となっています。

 ※ 型定義ファイル内の
    import 対象のパスに拡張子 .d.ts を加えている

    fp-ts の HKT で起きた問題を考えると、
    現時点では .d.ts ファイルの存在有無や
    package.json の typings フィールド等の考慮はされていないと考えられる
オリジナルの index.d.ts 内容
・・・
import * as alt from './Alt'
・・・
x-typescript-types の index.d.ts 内容
・・・
import * as alt from './Alt.d.ts'
・・・

ついでに GraphQL.js に対して確認してみましたが、こちらは(npm パッケージ内に .d.ts ファイルが存在するものの) ?dts を付けても x-typescript-types ヘッダーは付与されませんでした。

GraphQL.js の場合
$ curl --head -s https://cdn.skypack.dev/graphql?dts | grep x-typescript-types

(b) Deno Node compatibility の使用

Deno Node compatibility は Deno の std ライブラリに含まれている機能で CommonJS モジュールのロードだけではなく、Node.js API との互換 API もある程度は用意されているようです。(fs.readFile 等)

ただ、require の戻り値の型は any となってしまうので、TypeScript で使うメリットはあまり無いかもしれません。

b_1.ts
import { createRequire } from 'https://deno.land/std/node/module.ts'

const require = createRequire(import.meta.url)

const { some, none, map } = require('fp-ts/lib/Option')
const { pipe } = require('fp-ts/lib/pipeable')

const f = (d: any) => 
    pipe(
        d,
        map( (v: number) => v + 2),
        map( (v: number) => v * 3)
    )

console.log( f(some(5)) )
console.log( f(none) )

fp-ts を npm でインストールしてから deno run で実行します。 deno run で実行するには下記オプションを指定する必要がありました。

  • --unstable
  • --allow-read
  • --allow-env
b_1.ts 実行結果
> npm install fp-ts
・・・

> deno run --unstable --allow-read --allow-env b_1.ts
・・・
{ _tag: "Some", value: 21 }
{ _tag: "None" }