我が名はなんとか菜である!

主に技術系の記事を書きますが、ポエムも混入します。

Linter や自動フォーマッターで修正できないコーディングルールは敷くべきではないという話

プログラムは芸術作品じゃないんだよ。

追記: 一気に書き終えたあとタイトルをあまり回収していないただの愚痴になってしまったことに気が付きましたが、色々疲れてたせいだと思うのでその点了承して読み進めてください

今関わってるプロジェクトで、謎のコーディングルールが敷かれている。

それは以下のようなものだ。

  1. すべての React コンポーネントは、 Component という名前で named export する
  2. すべてのモジュールは namespace import し、 named import は使わない
  3. ただし、 styled-components については default import してもよい

一つずつ問題点を書き出してみる。

1. すべての React コンポーネントは、 Component という名前で named export する

わざわざ内容を解説するまでもなく一発で害悪だとわかるこのルールだが一応書いていくと、まずこのルールには明らかな問題点がある。

それは、IDEによる auto import の恩恵を全く得られないという点だ。

もしこのルールですべてのコンポーネントComponent という定義し、VSCode で補完させると以下のような悲惨な状態になる。

f:id:happo31:20220319165219p:plain

これは手元で再現したコードだが、問題のプロジェクトのコードでも実際にこのようになる。

とりあえずこのままでは明らかに通常の named import (import { Component } from './something')では扱えないことが分かる。

ではどう使うのかというと、これは 2. にも関連するのだが

import * as Something from '@components/something'

export const Component = () => {
  return <Something.Component />
}

と、このように namespace import を一つ一つ手で書いていくことになっている。

繰り返しになるが、このルールの上では IDEでの補完が一切利用できない。

なぜなら、通常の状態では TypeScript には namespace import を自動で書いてくれる機能はないからである。

なにやらそういう plugin を書いている人はいるみたいだが・・・。

Namespace Import に補完を効かせる TypeScript プラグインを作った

そもそも、namespace import は named export されたオジェクト群を一つの名前空間で扱うために利用するのが便利な使い方だと思うが、このやり方は手段と目的が逆転しているように感じる。

つまり、「namespace import じゃないと他の import したパッケージと名前が衝突するから仕方なく使う」のではなく 「namespace import があるのだからユニークな名前を付けなくてもいいよね」 というある種の怠慢でしかないように思える。

そもそも、ファイル名については特にケアしなくても衝突が起こらない程度の規模のプロジェクトなので、ファイル名は元からユニークなのである。ファイル名と同じ名前をコンポーネントに付けて、それを default export するというルールではダメなのだろうか。(というか僕はいつもそういうルールで書いてるが・・・)

// 例えばこれを Hoge.tsx という名前のファイルだとする。
const Hoge = () => {
  // something...
}
default export Hoge; // ファイル名と一致させる

これによりVSCode上でも補完が効くし、しかもきちんと自動で default import してくれる。

(GIFとか貼り付けようと思ったんだけどなんかGIF撮影ツールが動かないので無し)

言うまでもなく、こちらの書き方のほうが圧倒的に快適だし便利である。なぜ、現代のIDEに当たり前に備わっている機能を使わず、手で import 文を書かせるのか、全く意味が分からない。

2. すべてのモジュールは namespace import し、 named import は使わない

このルール、てっきりコンポーネントにだけ適用されるルールだと思っていたのだが、どうやら react-router-dom といった npm でインストールしたパッケージでも使う必要があるらしい。 バカか?

要するに IDEによる auto import の全否定」 である。

ここまでくるともはや病気である。

import * as React from 'react'
import * as ReactRouterDom from 'react-router-dom'
import * as MyComponent from './MyComponent'

const Component = () => {
  const navigation = ReactRouterDom.useNavigation() // 🤮
  const [count, setCount] = React.useState(0) // 🤮🤮
  React.useEffect(() => { // 🤮🤮🤮
    // ...
  }, [])
}

ていうか、この書き方をすると tree shaking 働かないんじゃね?と思ったんだけど、それについてはどうやら効くらしい。

ソースは webpack とかバンドラーについての記事とかいろんなところに転がってた記憶があるけど、さきほど紹介した記事にも同様のことが書かれている。再度引用。

Namespace Import に補完を効かせる TypeScript プラグインを作った

まあ、性能的に問題がなくてもアホみたいに書きづらいという問題点はあるわけだが。

3. ただし、 styled-components については default import してもよい

はい、やっぱり出ましたねこういう例外ルール。 クソルールって絶対例外事項あるよな

これは当然の話で、 styled-componentsdefault import されているオブジェクト経由でしか扱えないオブジェクト(styled.div とか)があるため、 namespace import だけでは利用できないためである ためである。

そもそも namespace import と default import でほぼ同じパッケージが扱えるのは、パッケージの index.ts で相互運用できるように export してくれているか、型定義ファイルを作った人が頑張ったか、あるいは tsconfig.jsonesModuleInterop: true になっているおかげでヘルパーメソッドが生成されているからである。

この辺の話を掘り下げていくと TypeScript が ESM と CommonJS を相互運用するためにどうやってトランスパイルしているかみたいな深淵の話になってくるので省略するが、まあ要するに人間がわざわざ import * as ReactRouterDom from 'react-router-dom' なんて書かなくてもよくなっていることが多い。

このルールを制定した人はどうやら 手でコードを書くことで生まれる自動生成では表現できない温もり を守ろうとしているらしい。

考察

さて、問題はどうしてこのようなルールが制定され、運用されるに至ったのかである。

当然、僕はこのルールをPRにて指摘を受けた際、上記のような話をスクリーンショット付きで反論したのだが、その際に頂いたありがたい回答は

特に理由もないし自分もやめたいが、以前からこのルールで書いていてコードを統一したいので、この書き方をしてほしい

とのことだった。(ていうかやめたいならやめろよ)

特に理由はないという点で腰が抜けてどこかにいってしまった。

ここでもっともらしい理由がいくつか挙げられていれば僕も納得したのだが、マジで理由がないらしい。以前からやっているという理由だけで続けるのは明らかに負の遺産になるフラグだと思うのだが?

一応僕のほうでいくつか利点を 無理やりでっちあげ 考えてみたので、これも列挙してみる。

1. すべての React コンポーネントは、 Component という名前で named export する

ファイル名を変えたくなった時でもファイルの中身をいじって export されてるオブジェクトの名前をいじらなくてもよいので手間が減る。
default export していると、ファイル名をいじったときは人間が気を付けて default export してるオブジェクトの名前も変える必要があり、少々面倒である。
でもどうせファイル名を基準に namespace 書いてるので更新の手間はいずれにせよ発生するししなによりいちいち namespace import を書く手間のほうが上です本当にありがとうございました。

2. すべてのモジュールは namespace import し、 named import は使わない

import を更新する必要がないのでPR見るときに先頭部分に差分が発生しないので見やすいみたいなことはあるかも。
あと、import 文がめちゃくちゃ分厚くなって実装までなかなかたどり着かないみたいなことも減る・・・?
でもそんなに大量のファイルに依存するのってエントリーポイントぐらいじゃね・・・?

3. ただし、 styled-components については default import してもよい

利点っていうか愚痴なんだけど、
このルールを「外部パッケージについては」に変えるだけでまだだいぶマシになると思うんだけど・・・そもそもルールが狂ってる=ルールを考えている人が狂っているので、狂人の考えは僕にはよくわからないです・・・。

おまけ

さて、だいぶここまで書いて疲れてきたのだが、最後に、完全に僕の憶測でかつ、そう感じる僕のセンスもだいぶ狂ってる可能性が非常に高いとある説を書いておく。完全にたわごとなので、ぜひ笑い飛ばしてほしい。

4. import * as XXX が並んでいて美しい

import * as React from 'react'
import * as ReactRouterDom from 'react-router-dom'
import * as MyComponent from './MyComponent'
import * as MyOtherComponent from './MyOtherComponent'
import * as MyOtherOtherComponent from './MyOtherOtherComponent'

...

・・・

・・・いや、まさかね?