LoginSignup
11
0

ISUCON13で遭遇したTypeScriptとES Modulesにまつわるエラーとその解決

Last updated at Posted at 2023-12-18

この記事は、株式会社ビットキー Developer Advent Calendar 2023 19日目の記事です。

tl;dr

ISUCON13で実際に遭遇した問題を題材に、tsconfig.jsonのlibに設定を追加してECMA Scriptの新しい機能を追加する方法と、Node.jsをNative ES Modulesとして動かすために、必要な設定変更と修正が必要なアプリケーションコードについてまとめました。

あらすじ

先月、ISUCON13に同僚とチームを組んで参加して来ました。実装言語は日々の業務で利用しているNode.jsを選択したのですが、構成の変更中にTypeScriptのコンパイルとES Modulesに関連した問題が発生し、苦戦しつつも解決できたので、備忘録的に共有します。

なお、この記事ではISUCON自体の解説はしませんので、ISUCONが何かを知りたい方はISUCON公式ホームページを参照してください。

ISUCON13 Node.js実装の解説

ISUCON13のNode.js実装はTypeScriptを事前にコンパイル1しない!

ISUCON13のNode.jsは、tscによるコンパイルを行わず、tsxによる実行時の変換でサーバーが実行されていました。

(参考)サーバー上でNode.jsのプロセスをps auxfオプションで表示したもの

isucon      1331  0.1  1.5 1095596 57520 ?       Ssl  01:20   0:00 npm run start
isucon      1346  0.0  0.0   2888  1664 ?        S    01:20   0:00  \_ sh -c tsx watch src/main.ts
isucon      1347  0.1  1.3 11476996 52428 ?      Sl   01:20   0:00      \_ node /home/isucon/webapp/node/node_modules/.bin/tsx watch src/main.ts
isucon      1358  0.3  1.9 65146028 72836 ?      Sl   01:20   0:01          \_ /home/isucon/local/node/bin/node --require /home/isucon/webapp/node/node_modules/tsx/dist/preflight.cjs --import file:///home/isucon/webapp/node/node_modules/tsx/dist/loader.mjs src/main.ts
isucon      1378  0.0  0.3 721044 12168 ?        Sl   01:20   0:00              \_ /home/isucon/webapp/node/node_modules/esbuild/lib/downloaded-@esbuild-linux-x64-esbuild --service=0.18.20 --ping

JavaScriptにコンパイルするぞ!

一般的には事前にJavaScriptにコンパイルした方が、サーバーのパフォーマンスが良くなるので、初手でTypeScriptのコードをJavaScriptにコンパイルする修正を入れることにしました。2

以下、実行環境は下記のバージョンを前提とします。
Node.js 20.9.0
TypeScript 5.3.2

遭遇したエラーと解決方法

エラーその1:error TS2551: Property 'toReversed' does not exist on type '{ username: string; score: number; }[]'. Did you mean 'reverse'?

TypeScriptでコンパイルするためにおもむろにtsconfig.jsonを開きます。

{
  "extends": "@tsconfig/recommended/tsconfig.json",
  "compilerOptions": {
    "types": ["bun-types"]
  }
}

記述はこれだけですが、"extends": "@tsconfig/recommended/tsconfig.json”は以下の設定を取り込みます。

extendsを展開した上で、"types": ["bun-types"]はbunを使わないため削除、出力先の指定に"outDir": "dist/"を追加してtscを実行します。

{
  "compilerOptions": {
    "target": "es2015",
+   "outDir": "dist/",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
-   "types": ["bun-types"]
  }
}
$ npx tsc
src/handlers/stats-handler.ts:83:31 - error TS2551: Property 'toReversed' does not exist on type '{ username: string; score: number; }[]'. Did you mean 'reverse'?

83       for (const r of ranking.toReversed()) {
                                 ~~~~~~~~~~

  node_modules/typescript/lib/lib.es5.d.ts:1365:5
    1365     reverse(): T[];
             ~~~~~~~~~~~~~~~
    'reverse' is declared here.

src/handlers/stats-handler.ts:244:31 - error TS2551: Property 'toReversed' does not exist on type '{ livestreamId: number; title: string; score: number; }[]'. Did you mean 'reverse'?

244       for (const r of ranking.toReversed()) {
                                  ~~~~~~~~~~

  node_modules/typescript/lib/lib.es5.d.ts:1365:5
    1365     reverse(): T[];
             ~~~~~~~~~~~~~~~
    'reverse' is declared here.


Found 2 errors in the same file, starting at: src/handlers/stats-handler.ts:83

error TS2551: Property 'toReversed' does not exist on type '{ username: string; score: number; }[]'. Did you mean 'reverse'?

Property 'toReversed' does not exist on typeのエラーが発生しました。
toReservedはECMAScript Proposalで2023年1月にStage 4に到達した配列要素を非破壊的に変更するメソッド群のひとつです。

コンパイラのターゲットで指定したECMA Scriptのバージョンにまだ追加されていない新しい機能を利用したい場合、tsconfig.jsonのlibに追加の設定を書くことができます。

{
  "compilerOptions": {
    "target": "es2015",
+   "lib": ["ES2023.Array"],
    "outDir": "dist/",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

これでJavaScriptにコンパイルできました。

# エラーにならない場合はコンソールに何も出力されない
$ npx tsc

エラーその2:Error [ERR_REQUIRE_ESM]

無事にコンパイルできたので、サーバーを実行します。

# 実際のサーバー起動コマンドはsystemdで管理されていますが説明のため簡略化します
$ node dist/main.js
/home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js:27
var async_1 = require("nanoid/async");
              ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /home/isucon/webapp/node/node_modules/nanoid/async/index.js from /home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js not supported.
Instead change the require of index.js in /home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/home/isucon/webapp/node/node_modules/hono-sessions/script/deps.js:27:15) {
  code: 'ERR_REQUIRE_ESM'
}

Node.js v20.9.0

Error [ERR_REQUIRE_ESM]

これは依存しているnpmパッケージの内、hono-sessionsはNative ES Modulesのみに対応しているため、デフォルトでNode.jsでは実行できません。

Node.jsのNative ES Modules対応の問題や背景はすでに様々な記事で解説がなされているので、記事末にまとめるにとどめ、ここでは深入りしません。

少なくともこの状態ではサーバーが実行できないため、サーバーもNative ES Modulesで実行されるように修正していきます。

まずは、package.jsonに”type”: “modules”を追加します。
これによって、拡張子がmts、mjsでなくてもES Modulesとして解釈されるようになります。

{
  "name": "isupipe",
  // 中略
+ "type": "module"
}

次に、tsconfig.jsonのmoduleNode16に変更します。
package.jsonが"type": "module"の場合、ES Modulesとして解釈します。

{
  "compilerOptions": {
    "target": "es2015",
    "lib": ["ES2023.Array"],
    "outDir": "dist/",
+   "module": "Node16",
-   "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

この状態でコンパイルするとimportの構文エラーが発生します。
ES Modulesはimport宣言で拡張子をつける必要があります。

$ npx tsc
src/handlers/livecomment-handler.ts:3:33 - error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../types/application.js'?

3 import { HonoEnvironment } from '../types/application'

# 以下、他にもたくさん

修正例

import { Context } from 'hono'
import { RowDataPacket, ResultSetHeader } from 'mysql2/promise'
+ import { HonoEnvironment } from '../types/application.js'
- import { HonoEnvironment } from '../types/application'
// 後略

また、ES Modulesでは__dirname__filenameそのままでは利用できないため次の行を追加します。

+ import { fileURLToPath } from "url";
+ import path from "path";
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = path.dirname(__filename);

これでNative ES Modulesでサーバーを実行できるようになりました!

まとめ

ISUCON13で実際に遭遇した問題を題材に、TypeScriptでECMA Scriptの新しい機能を追加する方法とNode.jsをNative ES Modulesで動かす方法を解説しました。

日常の業務では、何となくでしか理解していなかった、tsconfig.jsonについて振り返る良い機会になったと同時に、競技中は素早く対応できなかったため実力不足を思い知らされました。3

20日は @takuuuuuuu777 が担当します。お楽しみに!

Appendix:Node.jsのモジュールシステムの複雑さを解説する偉大な先人の記事

  1. TypeScriptをJavaScriptに変換することは厳密にはトランスパイルですがここではコンパイルで統一します

  2. 事前にコンパイルした結果、予想は外れてスコアは落ちました。どうして

  3. ISUCONは日々の業務を8時間に濃縮したコンテストとも言えますが、Node.jsの環境起因エラーまで体感できるとは思ってもいませんでした

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0