JavaScript/TypeScriptメモ

TypeScriptでESMで動くJavaScript製ライブラリ(remark-shortcodes)を使う

TypeScriptでESMで動くJavaScript製ライブラリ(remark-shortcodes)を使う

TypeScriptで、remarkと、そのプラグインremark-shortcodesを使おうと思ったところハマったのでメモ。

経緯は飛ばして、結論だけを見たいときには、まとめをご覧ください。

経緯

JavaScriptのライブラリを読み込みたいが型定義ファイルがないのでエラー

まずは、次のような最小限のコードで動くかどうか実験。

src/index.ts
import { unified } from 'unified';
import parse from 'remark-parse';
import shortcodes from 'remark-shortcodes';

const markdown = `# title
Example paragraph

[[ testTag id="abc" ]]
`;

const test = () => {
  const tree = unified().use(parse).use(shortcodes).parse(markdown);

  console.log(tree);
};

test();

上記を入力すると、VS Codeで次のようなエラーが発生。

Could not find a declaration file for module 'remark-shortcodes'. 'workdir/node_modules/remark-shortcodes/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/remark-shortcodes` if it exists or add a new declaration (.d.ts) file containing `declare module 'remark-shortcodes';`

「.d.ts」でimport文があるとグローバル宣言ができずエラー

remark-shortcodesの型定義ファイルがないので、作成する必要がありそう。

いろいろなやり方があるようですが、今回は、「index.ts」と同じ階層に「index.d.ts」を作成してみることに決定。

そこで、下記のような型定義ファイルを作成。

src/index.d.ts
import { Plugin } from 'unified';
import { Root } from 'remark-parse/lib';

declare module 'remark-shortcodes' {
  const shortcodes: Plugin<string[][] | void[], string, Root>;
  export default shortcodes;
}

それでもエラーが消えず。

Could not find a declaration file for module 'remark-shortcodes'. 'workdir/node_modules/remark-shortcodes/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/remark-shortcodes` if it exists or add a new declaration (.d.ts) file containing `declare module 'remark-shortcodes';`

たしか、importを書くと、単にdeclare moduleでは、globalな定義にならないのが原因と推測。

型定義ファイルをVS Codeでは認識したのにts-nodeで認識できずにエラー

そこで、型定義ファイルの書き方を少し修正して、importをdeclare moduleの内側に移動。

src/index.d.ts
declare module 'remark-shortcodes' {
  const shortcodes: import('unified').Plugin<
    string[][] | void[],
    string,
    import('remark-parse/lib').Root
  >;
  export default shortcodes;
}

これで、VS Codeではエラーが消えました。

ところが、ts-nodeを実行すると、下記のエラーが発生。

package.json
  "scripts": {
    "ts-node": "ts-node",
  },
> npm run ts-node ./src/index.ts

src/index.ts:3:24 - error TS7016: Could not find a declaration file for module 'remark-shortcodes'. 'workdir/node_modules/remark-shortcodes/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/remark-shortcodes` if it exists or add a new declaration (.d.ts) file containing `declare module 'remark-shortcodes';`

3 import shortcodes from 'remark-shortcodes';
                         ~~~~~~~~~~~~~~~~~~~

ts-nodeで、型定義ファイルが読み込めていない様子。

ライブラリでES modulesを使用しているためエラー

いろいろ調べていくと、独立した型定義ファイルをts-nodeで読み込ませるには--filesオプションを付ければ良いらしい。

参考:https://github.com/TypeStrong/ts-node#files

そこで、これを設定して再実行。

package.json
  "scripts": {
    "ts-node": "ts-node --files",
  },
> npm run ts-node ./src/index.ts

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: workdir\node_modules\unified\index.js
require() of ES modules is not supported.
require() of workdir\node_modules\unified\index.js from workdir\lib\postmd_remark\index.ts is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.        
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from workdir\node_modules\unified\package.json.

たしかに、型定義エラーは消えましたが、今度はES modules is not supportedというエラーが発生。

手元の設定ではCommon JSを使う設定になっており、それが原因でエラーになっているっぽい。

参考:TypeScriptのESMでハマる

ES Modules化したら型定義ファイルが読み込めないエラーが復活

先ほどのブログを参考に、設定を変更。

そして、ts-nodeではなく、node --loader ts-node/esm ./src/index.tsで呼び出すと良いらしいということで、試してみる。

package.json
  "scripts": {
    "ts-node": "ts-node --files",
  },
  "type": "module", //追加
tsconfig.json
  "compilerOptions": {
    "module": "esnext",  // ← 元々"module": "commonjs", だったのを修正
  }
> node --loader ts-node/esm ./src/index.ts

src/index.ts:3:24 - error TS7016: Could not find a declaration file for module 'remark-shortcodes'. 'workdir/node_modules/remark-shortcodes/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/remark-shortcodes` if it exists or add a new declaration (.d.ts) file containing `declare module 'remark-shortcodes';`

3 import shortcodes from 'remark-shortcodes';
                         ~~~~~~~~~~~~~~~~~~~

また、最初の「型定義ファイルを読み込めない症状」が復活してしまいました・・・。

ts-nodeを起動して、ようやくエラーが消えた

型定義ファイルを読み込みつつ、ES Moduleとして実行することができないかts-nodeのドキュメントを探していたら、ts-nodeコマンドの代わりにts-node-esmコマンドを実行する方法を発見。

参考:github ts-node#esm

package.json
  "scripts": {
    "ts-node-esm": "ts-node-esm --files",
  },
  "type": "module", //追加
> npm run ts-node-esm ./src/index.ts

{
  type: 'root',
  children: [
    {
      type: 'heading',
      depth: 1,
      children: [Array],
      position: [Object]
    },
    { type: 'paragraph', children: [Array], position: [Object] },
    { type: 'paragraph', children: [Array], position: [Object] }
  ],
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 5, column: 1, offset: 50 }
  }
}

これで、ようやく動きました。

まとめ

  • 型定義ファイルを読み込ませたい

テキトーな場所に「.d.ts」ファイルを作成し、ts-node起動時に--filesオプションを付ける

  • TypeScriptのコードを、ES Modulesで起動したい

package.json、tsconfig.jsonをES Modulesが動くように設定し、ts-node-esmコマンドを使う

最終的な設定

src/index.ts
import { unified } from 'unified';
import parse from 'remark-parse';
import shortcodes from 'remark-shortcodes';

const markdown = `# title
Example paragraph

[[ testTag id="abc" ]]
`;

const test = () => {
  const tree = unified().use(parse).use(shortcodes).parse(markdown);

  console.log(tree);
};

test();
src/index.d.ts
declare module 'remark-shortcodes' {
  const shortcodes: import('unified').Plugin<
    string[][] | void[],
    string,
    import('remark-parse/lib').Root
  >;
  export default shortcodes;
}
package.json
  "scripts": {
    "ts-node-esm": "ts-node-esm --files",
  },
  "type": "module",
tsconfig.json
  "compilerOptions": {
    "module": "esnext",  // ← 元々"module": "commonjs", だったのを修正
  }

あとは、下記のコマンドで、エラーなく実行できました。

npm run ts-node-esm ./src/index.ts