【TypeScript×React】import時に絶対パスで参照したい
はじめに
別の投稿でフォルダ構成について書きましたが、階層が深いとimport
文が
import * as Method from `../../../../../../../../../method`;
のようになる恐れがあります。このコードを深夜に書くと確実にバグを生み出します。
よって、これを
import * as Method from `@method`;
のように書きたいな、という話です。
開発環境
Windows 10
Node.js 18.15.0
TypeScript 4.9.5
React 18.2.0
Visual Studio 2022
crate-react-app 5.0.1
react-app-rewired 2.2.1
TypeScriptでのimportの書き方
以降、以下のようなフォルダ構成での話になります。
─ node_modules
─ src
├ utils
│ ├ methods1.ts // 関数 method1
│ └ methods2.ts // 関数 method2
└ features
├ App
│ ├ hooks
│ │ └ app.ts // 関数 hookApp
│ └ types
│ └ app.d.ts // 型 typeApp
├ FeatureA
│ ├ compenents
│ │ └ children
│ │ ├ childA1.tsx
│ │ ├ childA2.tsx
│ │ └ ・・・ childAn.tsx まである
│ └ hooks
│ └ featureA.ts // 関数 hookFeatureA
└ FeatureB
├ compenents
│ └ featureB.tsx
└ hooks
└ featureB.ts // 関数 hookFeatureB
外部ライブラリ
node_module
以下にある外部ライブラリのモジュールをインポートするとき、以下のように記述します。
import * as React from 'react'; // すべてのモジュールをインポート
import { useEffect } from 'react'; // 個別のモジュールをインポート
from
の後ろには、本来モジュールの格納場所を書くんですが、外部ライブラリの場合は上記のようなシンプルな書き方ができます。
上記の例ではreact
と書いていますが、実際はnode_module/@types/react/index.d.ts
が参照されています。仕組みはあまり理解できていませんが、node_module
以下のモジュールは、モジュール名をそのまま書くことで参照できるようです。
独自モジュール
上記のフォルダ構成においてchildA1.tsx
からfeatureA.ts
をインポートする場合は以下のように書きます。
import { hookFeatureA } from '../../hooks/featureA.ts';
同様にapp.ts
、app.d.ts
、methods1.ts
、methods2.ts
も追加でインポートしてみましょう。
import { hookFeatureA } from '../../hooks/featureA.ts';
import { hookApp } from '../../../App/hooks/app.ts';
import { typeApp } from '../../../App/types/app.d.ts';
import { method1 } from '../../../../utils/methods1.ts';
import { method2 } from '../../../../utils/methods2.ts';
何とか書けました。
このコードを書ききった時点で今日の仕事は終わりの気分ですが、続けてfeatureB.tsx
から同じモジュールをインポートするコードも書きます。
import { hookFeatureA } from '../../FeatureA/hooks/featureA.ts';
import { hookApp } from '../../App/hooks/app.ts';
import { typeApp } from '../../App/types/app.d.ts';
import { method1 } from '../../../utils/methods1.ts';
import { method2 } from '../../../utils/methods2.ts';
同じファイルを参照しているのにfrom
の右側の内容が違います。これは、参照元と参照先の相対位置が違うためです。
例えばここで、参照先のファイルの格納場所を変える仕様変更が発生したとしましょう。
当然エラーが発生しますが、どういった相対パスに修正すればエラーが消えるのか、フォルダ構成図とにらめっこしながら各ファイルの階層を意識して変更する必要が出てきます。
苦痛です。苦痛から解放されたいです。
第1段階:絶対パスみたいに書きたい
上記の2つの例ではどちらも、App
やutils
フォルダにある関数をインポートしています。
このように、ほとんどのコードでインポートが必要となる共通モジュールのようなものがあると思います。
これらのインポートを同じような書き方で統一できればキレイですよね。
例えばこんな風に。
import { hookFeatureA } from '../../hooks/featureA.ts';
import { hookApp } from '@app/hooks/app.ts';
import { typeApp } from '@app/types/app.d.ts';
import { method1 } from '@utils/methods1.ts';
import { method2 } from '@utils/methods2.ts';
import { hookFeatureA } from '../../FeatureA/hooks/featureA.ts';
import { hookApp } from '@app/hooks/app.ts';
import { typeApp } from '@app/types/app.d.ts';
import { method1 } from '@utils/methods1.ts';
import { method2 } from '@utils/methods2.ts';
下4つのインポートは、どちらも同じコードになっています。ステキ。
この書き方ができるようになるためには、以下の操作が必要となります。
- まずは'tsconfig.json'を修正します。
{
+ "extends": "./tsconfig.paths.json",
"compilerOptions": {
・・・
},
+ "include": [
+ "src"
+ ]
}
- 次に、同じ場所に
tsconfig.paths.json
を新規作成し、以下のような内容とします。
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@app/*": [ "./features/App/*" ],
"@utils/*": [ "./utils/*" ]
}
}
}
「"src"
を起点とし、そこからの相対パスを@xxx
という変数で表現する」といった意味になります。
create-react-appの場合
create-react-app
で実行している場合、上記の対応だけではうまくいかないはずです。
create-react-app
では、"compilerOptions"
に書いた独自の内容が、コンパイル時に初期状態にリセットされる挙動があるとか。
なので、もう少し対策します。
-
react-app-rewired
というライブラリをインストールします。
npm i react-app-rewired
-
package.json
を書き換えます。
・・・
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
+ "test": "react-app-rewired test",
- "test": "react-scripts test",
+ "eject": "react-app-rewired eject"
- "eject": "react-scripts eject"
},
・・・
- 最後に、
src
と同じ階層にconfig-overrides.js
を新規作成し、以下のような内容とします。
const path = require('path');
module.exports = (config) => {
config.resolve = {
...config.resolve,
alias: {
...config.alias,
'@app': path.resolve(__dirname, './src/features/App/'),
'@utils': path.resolve(__dirname, './src/utils/')
},
extensions: ['.js', '.ts', '.d.ts', '.tsx']
};
return config;
};
ちょっとここら辺の詳しい仕組みは理解し切れていませんが、いろいろやった結果 私の環境ではこれでtsconfig.paths.json
の"paths"
がしっかり反応するようになりました。
第2段階:まとめて書きたい
欲が出てくると、次に以下のようにまとめたい気持ちになってきます。
import { hookFeatureA } from '../../hooks/featureA.ts';
import { hookApp, typeApp } from '@app';
import { method1, method2 } from '@utils';
import { hookFeatureA } from '../../FeatureA/hooks/featureA.ts';
import { hookApp, typeApp } from '@app';
import { method1, method2 } from '@utils';
異なるファイルに書かれた関数を、1つのインポートで済ませる方法です。
- まずは、先ほど作成した
tsconfig.paths.json
を以下のように変更します。
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
- "@app/*": [ "./features/App/*" ],
+ "@app": [ "./features/App/" ],
- "@utils/*": [ "./utils/*" ]
+ "@utils": [ "./utils/" ]
}
}
}
- 次に、
features/App
とutils
フォルダ直下にそれぞれindex.d.ts
を新規作成し、以下のような内容とします。
export * from './hooks/app.ts';
export * from './types/app.d.ts';
export * from './methods1.ts';
export * from './methods2.ts';
こうすることで、
@app
→ ./features/App/
→ ファイルの指定が無ければデフォルトでindex
というファイルを参照する&config-overrides.js
の"extensions"
で.d.ts
の拡張子を対象としている
→ ./features/App/index.d.ts
→ export
で指定された各ファイルをすべて参照できる
という仕組みで、一度に複数ファイルの関数を参照できるようになります。
第3段階:やりすぎない
今回例に挙げたフォルダ構成は、冒頭に紹介した記事からいくつか抜粋したものになります。
実際は、もっとたくさんのフォルダがあり、階層があり、相互参照が発生しています。
では、すべて絶対参照形式で書けば楽になるか?というと、必ずしもそうはなりません。
例えば、ここまでの方法で"@features/*": [ "./features/*" ]
を定義したとしましょう。
-import { hookFeatureA } from '../../hooks/featureA.ts';
+import { hookFeatureA } from '@features/FeatureA/hooks/featureA.ts';
import { hookApp, typeApp } from '@app';
import { method1, method2 } from '@utils';
確かに見やすくはなりましたが、長いし、同じFeatureA内のモジュールを参照したいだけなのにパスに"FeatureA"と書かなければならないことに少し違和感を感じます(個人的な感想です)。
よって、結論として、
- 同じ機能内であれば相対パスでインポートする。
- 外にあるモジュールは絶対パスでインポートする。
のルールで統一しようと思います。
以上です。
P.S.
この投稿日、41歳の誕生日です。
いつまで技術職ができるか不安ですが、これができないと管理職か営業職なので、がんばろうと思います。
Discussion