Deno から npm パッケージを使用する(Deno で fp-ts)
下記の方法を用いて Node.js / ブラウザ用 npm パッケージを Deno から利用してみました。
- (a) Skypack の使用
- (b) Deno Node compatibility の使用
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-ignore
は Visual Studio Code におけるエラー表示対策 ※ のために付けています。
※ pipe 関数は、10個の any 型の引数をとる関数となっているが、 引数を 3つしか指定していない事に対するエラー
Visual Studio Code におけるエラー表示例(@ts-ignore を付けなかった場合)
なお、関数の引数や戻り値などを適切な型で扱うには、型定義ファイル(.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.ts
や pipeable.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" }