自主的20%るぅる

各々が自主的に好き勝手書くゆるふわ会社ブログ

TypeScript の paths はパスを解決してくれないので注意すべし!

こんにちは、江嵜です。

みなさん、 TypeScript 書いてますか?

私は JavaScript のゆるふわ感が大好きなんですが、
やっぱりそこそこの大きさのものを開発しようと思うと
型とかあった方が安心で、TypeScript を使おうかなって気になります。

で、今回はそんな TypeScript を使う上で注意すべき小ネタ的お話を 1 点。
(文中のコードは一部全角にしているので実行される場合は注意してくださいね)

TypeScript の paths って設定使ってますか?

TypeScript はコンパイルのルールを色々設定することができます。
tsconfig.json ファイルですね!
多分一度 TypeScript を書かれた方なら触ったことがあるのではと思います。

色々な設定ができるのですが、今回はその中でも
paths という設定項目について、見ていきます。

…と!その前に、今回の環境を準備

さて、すぐにでも paths のお話をしてもよいのですが、
ここは検証のための環境を簡単に作っておきましょう。

とりあえず、こんなディレクトリ構成で…

test
├ node_modules
├ index.ts
├ package.json
└ tsconfig.json

package.json はこんな感じ。
グローバルインストールがイヤなので、
npm install --save-dev typescript で typescript だけ入れておきます。

{
  ”name”: ”test”,
  ”version”: ”1.0.0”,
  ”description”: ””,
  ”main”: ”index.js”,
  ”scripts”: {
    ”build”: ”tsc -p tsconfig.json”
  },
  ”author”: ”ezaki”,
  ”license”: ”No License”,
  ”devDependencies”: {
    ”typescript”: ”^3.3.3333”
  }
}

tsconfig.json はこう。
これも基本的な設定だけで特に何ってことはないですね。
ちなみに今回はサーバーサイドで使うことを想定しているので、
target は ES5 じゃなくてもいいかなという事で ESNext で。

{
  ”compilerOptions”: {
    ”module”: ”CommonJS”,
    ”target”: ”ESNext”,
    ”noImplicitAny”: true,
    ”rootDirs”: [
      ”.”
    ],
    ”moduleResolution”: ”Node”,
    ”sourceMap”: true,
    ”baseUrl”: ”.”
  },
  ”include”: [
    ”./**/*”
  ],
  ”exclude”: [
    ”./node_modules”
  ]
}

index.ts はとりあえずこう。

const hello = (name: string): void => {
  console.log(`Hello! ${name}`);
};

hello(’AG’);

JavaScript に、ただ型情報を与えただけですね。

では、コンパイルしてみましょう。
package.json に build コマンドを作っておいたので、

npm run build

でいいですね。

こんな感じで、ビルドできます。
<img src=”https://www.agent-grow.com/self20percent/wp-content/uploads/2019/03/0d40a5e4a645fc6b96e767d64ac0878e.png” alt=”” width=”284” height=”77” class=”alignnone size-full wp-image-13306” />

ディレクトリ内はこんな感じ

test
├ node_modules
├ index.ts
├ index.js
├ index.js.map
├ package.json
└ tsconfig.json

index.jsindex.js.map が生成されましたね。
これを node コマンドで実行。

node index.js

””

はい、想定通り実行されました。

paths を使ってみる

ここまでは普通ですからね!
では、本題の paths を使ってみましょう。

この paths というのは、公式ドキュメントの説明としては

List of path mapping entries for module names to locations relative to the baseUrl.

baseUrl からのモジュール名への相対パスマッピングということで。

これだけだと良くわからんので実例を。

例えばこんな構成だったとして…

test
├ node_modules
├ index.ts
├ package.json
├ tsconfig.json
└ a
  └ b
    └ c
      └ d
        └ name.ts

index.ts は name.ts に依存しているとしましょう。

name.ts はこんな感じ

export const MyName = ’AG’;

MyName を export してるだけですね。

これに合わせて、 index.ts も修正しましょう。

import { MyName } from ’./a/b/c/d/name’;

const hello = (name: string): void => {
  console.log(`Hello! ${name}`);
};

hello(MyName);

先程同様、ビルドして実行してみると、同じように ”Hello! AG” が出力されますよね。

良かった良かった…なのですが、

import { MyName } from ’./a/b/c/d/name’;

この部分長くないですか?

今回みたいなケースならまだいいのですが、
lib みたいなディレクトリが浅い階層にあって、
色々な所から

import { MyName } from ’../../../../../../lib’;

みたいにすごくさかのぼって参照しないといけないとか…
イヤですよね!私はイヤです!

ということで、これを解決してくれるのが、この paths です。

こんな感じで paths の設定を追加すれば、
import の対象をルールに従って読み替えてくれます。

{
  ”compilerOptions”: {
    ”module”: ”CommonJS”,
    ”target”: ”ESNext”,
    ”noImplicitAny”: true,
    ”rootDirs”: [
      ”.”
    ],
    ”moduleResolution”: ”Node”,
    ”sourceMap”: true,
    ”baseUrl”: ”.”,
    ”paths”: {
      ”@/*”: [”./a/b/c/d/*”],
      ”@”: [”./a/b/c/d”]
    }
  },
  ”include”: [
    ”./**/*”
  ],
  ”exclude”: [
    ”./node_modules”
  ]
}

今回、 @/* を設定しました。
* の部分は任意の文字なので、ちょうど @/./a/b/c/d/ と差し変わる形ですね。

では、これに合わせて、 index.ts も修正してみます。

import { MyName } from ’@/name’;

const hello = (name: string): void =&gt; {
  console.log(`Hello! ${name}`);
};

hello(MyName);

スッキリしましたね!

こんな感じで、よく使うパスを特別に設定しておくことで、
カンタンに import を書くことができるようになります。

ちなみに、 paths の設定を外して動かしてみると…

””

「@/name というモジュールが見つからんよ」
とエラーを出してくれます。
TypeScript はこういう感じで、存在しないものを設定したときに
きちんとエラーを出してくれるというのがいいところですね。

でもこれ…実行できない?

ワーイヤッターベンリー!なんですが、
ここで残念なお知らせを一つ…

これ、実行できないんです。

試しに実行してみると…

””

あれ? @/name モジュールが無いと言われてますね…

生成された index.js を見てみると…

”use strict”;
Object.defineProperty(exports, ”__esModule”, { value: true });
const name_1 = require(”@/name”);
const hello = (name) =&gt; {
    console.log(`Hello! ${name}`);
};
hello(name_1.MyName);
//# sourceMappingURL=index.js.map

require(”@/name”) と、パスが @/ のまま入っているではないですか!
解決してくれたんじゃないんかい!

…と、思いますが、これが TypeScript の仕様です。
ちなみに、今後も typescript 側で解決はしてくれるようにはならなさそうです。

この辺りの経緯は typescript の GitHub issue で
https://github.com/Microsoft/TypeScript/issues/10866
こんな感じで議論されていましたが、最近、議論をロックされました。

最終的な結論としては、
「import のパス解決は TypeScript の責務じゃないから!」
という事みたいです。

確かに、 TypeScript としてはファイルがあることを確認してくれればいいわけで、
JavaScript にコンパイルした後のパス解決は TypeScript のやることではないと。
ド正論でしたね…

では、どうするのか、というと、
「webpack とか、そういうパス解決させるためのヤツにやらせなさい」
という事みたいです。

ということで、そうしましょうか。

npm install --save-dev webpack webpack-cli ts-loader
webpack と typescript を webpack から使うための loader ををインストールして、
webpack.config.js をこんな感じでルートディレクトリに作っときましょう。

const path = require(’path’);

module.exports = {
  mode: ’development’,
  entry: path.resolve(__dirname, ’index.ts’),
  output: {
    path: path.resolve(__dirname),
    filename: ’main.js’,
  },
  resolve: {
    extensions: [’.ts’],
    alias: {
      ’@’: path.resolve(__dirname, ’a/b/c/d’),
    },
  },
  target: ’node’,
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: ’ts-loader’,
          options: {
            configFile: path.resolve(__dirname, ’tsconfig.json’),
          },
        },
      },
    ],
  },
};

後は package.json の ”build” の所を、
”tsc -p tsconfig.json” から ”webpack” に変えて…

””

生成された main.js を実行してみれば、
きちんとビルドも実行もできていますね!

まとめ

TypeScript に paths というオプションがあるので、
なんとなく解決までやってくれそうなイメージですが、
TypeScript の責務を考えればそこまでやらないというのも分かる話ですね。

ちなみに私は
「どこかに解決までしてくれるオプションとかあるんじゃねぇの!」
と思って 1 時間程無駄にしました。ご報告まで。

Let’s share this article!

{ 関連記事 }

{ この記事を書いた人 }

アバター画像
takato_ezaki
記事一覧