お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
言語::TypeScript

五年くらい公私ともにTypeScriptを書いているが、なんか最近限界を感じたので吐露していく。半分くらいNode.jsに対する不満かもしれない。内容としてはただの書きなぐりの愚痴。

非同期制御多すぎ

asyncawaitを書く面倒さや、それが漏れた時のふるまいの微妙さ。

例えば終わらないJestというのは日常的に見る光景だと思うし、実務をしていてPromiseの例外ハンドリングをしていなかったからサーバーがクラッシュしたというのも聞いたことがある。

これはチーム開発において、チームの意識や技術レベルが低いと容易に引き起こされる問題であり、その面で考えると非常に微妙な言語仕様だと思う。

なんだかんだC10K問題はコンテナ大量起動で回避する世界線になってしまっている気がするので、標準が非同期である必要性はあんまないよねみたいな感触は抱いている。C10K問題回避のために非同期にしてるんだよね?そうだよね?(正直非同期にしたいのであればユーザー側がPromiseに包めばいいと思う)

Node.jsのLTS短すぎ

GitHubやAWSですら最新版への追従は遅れがちなくらいLTSが短い。2年くらいしかない。2年ごとにフルリグレッションなんてやってられない。不可能だ。

しかも破壊的変更を平気で入れてくるし、v16は予定より早くサポートが打ち切られた。正気とは思えない。

実行環境多すぎ

Node.js + 各種トランスパイラにDenoにBunに…他にも最近なんか出ていた気がするが忘れた。なんでこんなにあるのか…。

TypeScriptの型パズルに疲れた

型パズルするために開発してるんじゃないんだよ…。

型が信用できない

実務だと意図しているのか、していないのか型を騙している光景を多々見るが、これによって型が信用できない状態になっていることが往々にしてあり、割と殺意を覚える。なにも信用できない。何のためにこの言語使ってるんだっけ…?ナウいから?

CLI作る時のハマりどころが多い

nvmのようなバージョン管理システムを使っているとグローバルインストールが通常ユーザーとスーパーユーザーで別れる問題があり、sudo実行が前提のCLIで誤った動作確認をしてしまいやすい。

他にもnpmjsで公開しているパッケージをnpm installしたものと、開発中に実行するのでは、意識してないと実行方式が変わってしまい、上手く動かないことがある。気を付ければいいのはそうだが、割とムカつく。

具体的にはshebang書いて無くて動いてなかったとかそういうやつ。基本的にjsファイルを直に叩いて動かす文化ないじゃん…。

ビルド工程が面倒くさい

ビルドに関してtscやswc、esbuildのように様々なトランスパイラが存在するほか、webpackやvite, esbuildといったバンドラの存在があり、バージョンアップで振る舞いが変わるので面倒くさい。TypeScript開発は膨大なパッケージに依存しがちで、何か不具合が起きた時に原因の特定が困難だが、根幹に破壊的変更が来ると割と精神的にきつい。

先ほどswcでビルドしたところ/dist/index.jsとして出てくるはずのものが、/dist/src/index.jsになっていた。正直こんなのはデグレードだ。ありえない。

もちろんこれはfix: pass in --strip-leading-paths to swc when buildingで対応されているのだが、正直しんどい。

てかNode.js系のパッケージって息を吸うようにデグレ起こすような気がするのは私だけだろうか。以前はNewRelicがプリフェッチしまくるバグとかあった気がするし、Jestのモックが正常に動かないケースにも遭遇したことがある。

エコシステムが壊れかかっている

TypeScriptでLintをするとなればESLintがデファクトスタンダードだと思うが、v8で導入されたFlat configがあまりにも不案内で、v9.7が出た今でさえ対応できていないPluginがゴロゴロある。むしろできてるほうが珍しい。

私も三日くらい格闘したが、いずれかのプラグインが不整合を起こすか、不整合を起こさなくなったと思ったら無視設定が機能しなかったり、今度はLint機能そのものが死んだりで手に負えないのであきらめた。v9に上げると使えなくなるプラグインもあるので、v8.57.0で旧式の設定を使うのがきっと無難だと思うが、これがいつまで続くのかという話だ。恐らく追従できないプラグインはサポートのやる気をなくすだろう。

Prettierも破壊的変更を繰り返しフォーマットをガタガタにしてくる困りものだ。こいつもv3が出て一年たった今でもVSCodeの公式拡張機能がv3に対応していない。一応ローカルにv3を入れれば最低限動くが、ネイティブで動いてほしいケースもあるので、そういう場合は運用に難儀する。過去に対応させようと手を付けてみたが、最新版はテストが落ちる状態になっていて、もはや救いがなかったので投げた。

正直ESLintとPrettierが死にかけている現状を見るに、もうなんというか駄目じゃないかという気がしてくる。

Biome.jsなんてのもあるが、あれはMarkdownのフォーマットができないとか、ESLintのプラグインをサポートしてないとかで使い勝手がいまいちよくないのだ。まぁ、MarkdownのフォーマットだけPrettier使うとかはアリか。

あとがき

とはいえ、フロントエンド開発において、ブラウザではJavaScriptしか動かないのでTypeScriptをの利用はやむを得ない側面もある。やむを得ないのだ。ただ、それ以外ではもう使う必要ないんじゃないかなと思うし、正直Node.js界隈が魔境というか地獄過ぎて、あんまり関わり合いになりたくないという思いが日に日に強くなっている今日この頃だ。

投稿日:
言語::TypeScript

process.exit()のラッパーだったり、throwする関数だったりして、戻り値がneverを取る関数で、TypeScript上、後続処理がデッドロジックになるものを作る方法。

確認環境

Env Ver
TypeScript 5.4.5

やりたいこと

process.exit()を書くと次の行以降がデッドロジック扱いされるが、これをやりたい。

process.exit()を書くと次の行以降がデッドロジック扱いされる

問題点

普通に実装して型推論に任せてもうまくいかない。

例えば次の左図の関数の戻り値型はneverだが、右図ではデッドロジックとならない。

関数の戻り値型はnever
neverなのに後続がデッドロジック扱いされない

これは次のように戻り値の型にneverを直書きしても機能しない。

戻り値の型にneverを直書きしても機能しない

解決策

次の左図のように関数そのものの型を作ると解決できる。右図を見るとデッドロジックになっていることが確認できる。

関数そのものの型を作る
関数そのものの型があれば、後続もデッドロジック扱いされる

何のためにこれをやるか

別の関数から呼んだときに、呼び側の関数の戻り値型を正しくするためだ。下図はデッドロジックになっていない状態のときのものだが、処理が死なずに通り抜けてしまうのでundefinedが帰ることになっており、この関数の呼び下でundefinedだった時の処理を書く必要が出てきてしまう。

デッドロジック扱いされない状態では戻り値の状態が不正になる

しかし、デッドロジック扱いになっていれば下図のように戻り値の型が期待した通りの内容になる。この関数がundefinedを返すことはないので、この状態を実現するために行っている。

paste-image-2024-25-8_12-24-37-594.png

関連記事

投稿日:
言語::TypeScriptNode.js::ESLintNode.js::Jest

依存ライブラリの関係でESLint v9への移行はできていないが、取り合えずFlat configに移行した。ESLintは7.9.0から8.57.0に更新している。

確認環境

Flat configになってからeslint:recommendedは@eslint/jsに移った模様。

Env Ver
@eslint/js 9.2.0
@lycolia/eslint-config 0.9.1
@swc/jest 0.2.31
@types/jest 29.5.11
@types/node 20.11.5
eslint 8.57.0
eslint-plugin-jest 28.5.0
jest 29.7.0
prettier 3.2.5
typescript 5.4.5
typescript-eslint 7.8.0

元の設定

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:jest/style",
    "plugin:jest/recommended",
    "@lycolia/eslint-config",
    "prettier"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

やったこと

ESLintなど必要ライブラリのアップデート

npm i -D eslint @eslint/js eslint-plugin-jest jest prettier typescript typescript-eslint @lycolia/eslint-config

新設定の作成

eslint.config.jsを作成

extendsの設定

今までextendsに書いていたものはrequireでモジュールを参照してmodule.exports = []の配列の中に書いた。typescript-eslintとeslint-plugin-jestの対応が何とも言えない。なお、この状態だとtypescript-eslintが正常に動かない。

const eslint = require("@eslint/js");
const eslintTypeScript = require("typescript-eslint");
const eslintJest = require("eslint-plugin-jest");
const eslintLycolia = require("@lycolia/eslint-config");

module.exports = [
  eslint.configs.recommended,
  eslintTypeScript.configs.recommended[0],
  eslintJest.configs["flat/style"],
  eslintJest.configs["flat/recommended"],
  eslintLycolia,
];

設定が競合して都合が悪かったので、この時にPrettierの設定を削除しているが、Flat config化とは特に関係ないので無視してよい。

@lycolia/eslint-configは私が作っているカスタム設定だが、これは特に何もせずとも使えた。

.eslintignore対応

extendsしたオブジェクトの次にオブジェクトを記述し、そこにignores: []を追加し、その中に同じことを書いた。恐らく根っこの配列はESLintの設定オブジェクトの羅列で、最終的に後勝ちでマージされる様に出来ているのだろう。

const eslint = require("@eslint/js");
const eslintTypeScript = require("typescript-eslint");
const eslintJest = require("eslint-plugin-jest");
const eslintLycolia = require("@lycolia/eslint-config");

module.exports = [
  eslint.configs.recommended,
  eslintTypeScript.configs.recommended[0],
  eslintJest.configs["flat/style"],
  eslintJest.configs["flat/recommended"],
  eslintLycolia,
  {
    ignores: [
      'dist/*',
      'coverage/*',
      '**/*.d.ts',
      '/src/public/',
      '/src/type',
      '*.config.js'
    ],
  }
];

TypeScript対応

eslintTypeScript.config()でESLintの設定配列全体を引数としてラップし、languageOptionsの中身を移植した。旧設定にあったplugins公式のドキュメントを見る限り不要になっている模様

const eslint = require('@eslint/js');
const eslintTypeScript = require('typescript-eslint');
const eslintJest = require('eslint-plugin-jest');
const eslintLycolia = require('@lycolia/eslint-config');

module.exports = eslintTypeScript.config(
  eslint.configs.recommended,
  ...eslintTypeScript.configs.recommended,
  eslintJest.configs['flat/style'],
  eslintJest.configs['flat/recommended'],
  eslintLycolia,
  {
    ignores: [
      'dist/*',
      'coverage/*',
      '**/*.d.ts',
      '/src/public/',
      '/src/type',
      '*.config.js'
    ],
    languageOptions: {
      parser: eslintTypeScript.parser,
      parserOptions: {
        project: './tsconfig.json'
      }
    }
  }
);

旧設定の削除

.eslintrc.eslintignoreを消した。

ESLint v9対応

typescript-eslintが対応していないため、現時点ではできなかった。

変更差分まとめ

-{
-  "extends": [
-    "eslint:recommended",
-    "plugin:@typescript-eslint/recommended",
-    "plugin:jest/style",
-    "plugin:jest/recommended",
-    "@lycolia/eslint-config",
-    "prettier"
-  ],
-  "plugins": [
-    "@typescript-eslint"
-  ],
-  "parser": "@typescript-eslint/parser",
-  "parserOptions": {
-    "project": "./tsconfig.json"
+const eslint = require('@eslint/js');
+const eslintTypeScript = require('typescript-eslint');
+const eslintJest = require('eslint-plugin-jest');
+const eslintLycolia = require('@lycolia/eslint-config');
+
+module.exports = eslintTypeScript.config(
+  eslint.configs.recommended,
+  ...eslintTypeScript.configs.recommended,
+  eslintJest.configs['flat/style'],
+  eslintJest.configs['flat/recommended'],
+  eslintLycolia,
+  {
+    ignores: [
+      'dist/*',
+      'coverage/*',
+      '**/*.d.ts',
+      '/src/public/',
+      '/src/type',
+      '*.config.js'
+    ],
+    rules: {
+      semi: ['error', 'always']
+    },
+    languageOptions: {
+      parser: eslintTypeScript.parser,
+      parserOptions: {
+        project: './tsconfig.json'
+      }
+    }
   }
-}
+);
投稿日:
言語::TypeScriptジャンル::思考

ここ5年間くらいTypeScriptを書くことが多いが、何故TypeScriptで書いているかについてメリデメを大まかに整理してみる。

メリットに感じている部分

  1. 静的型付けがある
    • IDEで入力保管が出るのは便利だし、型ミスによるバグも見つけやすい
  2. マルチプラットフォーム
    • OSの差を意識せず開発しやすい
  3. VSCodeで書きやすい
    • IntelliJ使いたくないので…
  4. テストコードを書くのが楽
    • Jestは扱いやすい
  5. 大抵のことができる
    • CLIアプリからWebサーバー、Webフロントエンドまでこれ一本でできるのは嬉しい

デメリットに感じている部分

  1. 依存地獄
    • node_modules配下が地獄になりがち
  2. Node.jsの頻繁なバージョンアップ
    • TypeScriptはNode.jsに依存しているため、どうしてもこの部分に引っ張られがち(DenoとかBunとかそういうのは関知しない)
      • fsを中心に破壊的変更がよくあり、割合壊れる
  3. デバッグがめんどくさい
    • C#だとデバッガを上げるのは楽だがNode.jsはめんどくさい
    • たまに素直にブレークポイントにはまってくれないこともあり辛い
  4. 言語エンジンが重い
    • 入力補完や型情報の照会などはコードが肥大化するにつれ重くなる
    • そのためにキャッシュ機能があるようだが、ブランチを切り替えるとキャッシュされた型情報を見てくるせいでうまく動かないなど面倒
投稿日:
言語::TypeScriptNode.js::Jest開発::テスト

関数の仮引数にthisが入っているタイプの奴。FastifyのsetErrorHandlerに渡す関数のテストをする時に詰まったので書いておく。滅多に遭遇しないケースだと思う。

確認環境

Env Ver
jest 29.7.0
typescript 5.3.3

実装コード

export class HogeServer {
  constructor() {
    console.log();
  }
}

interface HogeServerInstance {
  setErrorHandler(
    handler: (
      this: HogeServer,
      error: unknown,
      callback: (code: number) => void
    ) => void
  ): HogeServer;
}

// 型定義にthisが入っていてテスト時に上手くいかない
// const handle: (this: HogeServer, error: unknown, callback: (code: number) => void) => void
type HandlerFunc = Parameters<HogeServerInstance['setErrorHandler']>[0];

export const handle: HandlerFunc = (err, cb) => {
  if (err instanceof Error) {
    cb(500);
  } else {
    cb(200);
  }
};

export const fuga = (s: HogeServerInstance) => {
  s.setErrorHandler(handle);
};

テストコード

import { HogeServer, handle } from '.';

describe('handle', () => {
  it('errがErrorインスタンスの時', () => {
    const thisMock = {} as unknown as HogeServer;
    const err = new Error('error-hoge');
    const cbFn = jest.fn();
    // .call()を使うことでthis付きで呼び出せる
    handle.call(thisMock, err, cbFn);

    expect(cbFn).toHaveBeenCalledWith(500);
  });

  it('errがErrorインスタンスではない時', () => {
    const thisMock = {} as unknown as HogeServer;
    const err = 1234;
    const cbFn = jest.fn();
    handle.call(thisMock, err, cbFn);

    expect(cbFn).toHaveBeenCalledWith(200);
  });
});