30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

babel-loader で TypeScript をビルドする

最小構成のサンプルから始めて、React のビルドや Babel プラグインの利用も出来るようにしていく。

当初はts-loaderbabel-loaderを組み合わせてビルドする予定だったのだが、Babel プラグインが動かなかったので断念した。この資料の内容に沿って設定してもダメだった。
そのため、babel-loaderのみでビルドすることにした。

使用しているライブラリのバージョン。

  • webpack@4.30.0
  • webpack-cli@3.3.2
  • babel-loader@8.0.5
  • @babel/core@7.4.4
  • @babel/preset-env@7.4.4
  • @babel/preset-typescript@7.3.3
  • typescript@3.4.5
  • react@16.8.6
  • react-dom@16.8.6
  • @types/react@16.8.17
  • @types/react-dom@16.8.4
  • @babel/preset-react@7.0.0
  • babel-plugin-react-remove-properties@0.3.0

対象ファイルの作成

以下の内容のsrc/index.tsを作成する。TypeScript の拡張子は.tsなので注意する。

const value: number = 1;
console.log(value);

: numberという JavaScript にはない記法を使っているので、JavaScript ファイルとして実行することは出来ない。

$ node src/index.ts
> SyntaxError: Missing initializer in const declaration

まずはこのシンプルな TypeScript ファイルを、JavaScript 実行エンジンが処理できる形にビルドする。

必要なライブラリをインストールする

$ yarn init -yでプロジェクトを開始して、必要なライブラリをインストールしていく。

まずは webpack。

$ yarn add -D webpack webpack-cli

次に、babel-loaderとそれを使用するためのライブラリ。
ここで入れている@babel/preset-typescriptが、TypeScript のビルドを担う。

$ yarn add -D babel-loader @babel/core @babel/preset-env @babel/preset-typescript

設定ファイルを作成してビルドする

webpack.config.jsを作成。

const path = require('path');

module.exports = {
  entry: {
    main: './src/index.ts',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dest'),
  },
  module: {
    rules: [
      {test: /\.ts$/, use: ['babel-loader']}
    ],
  },
}

src/index.tsをエントリポイントとして、dest/main.jsを出力する。
.tsファイルに対してbable-loaderを使用する。

これで、src/index.tsに対してbabel-loaderを実行した結果がdest/main.jsとして出力されるようになったので、あとは Babel の設定ファイルを書けばよい。

以下の内容でbabel.config.jsを作成する。

const presets = [
  [
    '@babel/preset-env',
    {
      targets: {
        ie: '11',
        safari: '7',
      },
    },
  ],
  ['@babel/preset-typescript'],
];

const config = {
  presets,
};

module.exports = config;

@babel/preset-envの設定内容は何でもいいのだが、今回はこのような内容にした。

ビルドのためのnpm scriptsを定義して、それを実行する。

diff --git a/package.json b/package.json
index 4204881..963f549 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,9 @@
   "version": "1.0.0",
   "main": "index.js",
   "license": "MIT",
+  "scripts": {
+    "build": "webpack --mode=production"
+  },
   "devDependencies": {
     "@babel/core": "^7.4.4",
     "@babel/preset-env": "^7.4.4",
$ yarn build

これで、dest/main.jsが生成される。
JavaScript としてビルドされたので、JavaScript 実行エンジンが処理できるようになった。

$ node dest/main.js 
1

型チェックも行うようにする

これでビルドは出来るようになったのだが、型チェックは行われない。
babel-loaderはあくまでもファイルの変換だけを行い、型については見ていないためである。

例えば、src/index.tsを以下の内容にする。

const value: number = 'foo';
console.log(value);

numberであるべきvalueに文字列を代入しているが、問題なくビルドできてしまう。

$ yarn build
$ node dest/main.js 
foo

これでは TypeScript を利用する意味がないので、型チェックを行うための設定も追加する。

まず、typescriptをインストールする。

$ yarn add -D typescript

次に、TypeScript の設定ファイルであるtsconfig.jsonを作成する。
公式ブログの内容を参考にした。

{
  "compilerOptions": {
    "target": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "strict": true,
    "isolatedModules": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

これで$ yarn tscを実行すると、型チェックが実行される。

src/index.ts:1:1 - error TS1208: Cannot compile namespaces when the '--isolatedModules' flag is provided.
src/index.ts:1:7 - error TS2322: Type '"foo"' is not assignable to type 'number'.

エラーが2つ出た。TS2322valueに数値を代入するようにすれば直る。
TS1208は、valueをエクスポートすることで直る。

const value: number = 1;
console.log(value);
export default value;

例えばビルドの前にtscを実行することで、型が合っていないのにビルドされるのを防げる。

diff --git a/package.json b/package.json
index b183a11..5f112b0 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "scripts": {
-    "build": "webpack --mode=production"
+    "build": "tsc && webpack --mode=production"
   },
   "devDependencies": {
     "@babel/core": "^7.4.4",

React + TypeScript

次に、React をビルドできるようにする。

まずはindex.htmlを作成する。
このファイルがdest/main.jsを読み込み、React で作ったアプリを表示する。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>TypeScript</title>
</head>
<body>
<div id="app"></div>
<script src="./dest/main.js"></script>
</body>
</html>

React をインストール。

$ yarn add react react-dom

jsxを扱うファイルは拡張子を.tsxにするので、src/index.tsxを作成する。

import React from 'react';
import ReactDOM from 'react-dom';

const App = () => <div>Hello!</div>;

ReactDOM.render(
  <App />,
  document.querySelector('#app')
);

webpack.config.jsを、.tsxに対応させる。

diff --git a/webpack.config.js b/webpack.config.js
index f8b09f6..be31412 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -2,7 +2,7 @@ const path = require('path');
 
 module.exports = {
   entry: {
-    main: './src/index.ts',
+    main: './src/index.tsx',
   },
   output: {
     filename: '[name].js',
@@ -10,7 +10,7 @@ module.exports = {
   },
   module: {
     rules: [
-      {test: /\.ts$/, use: ['babel-loader']}
+      {test: /\.*(ts|tsx)$/, use: ['babel-loader']}
     ],
   },
 }

tsconfig.jsonにも、jsxオプションを追加する。

diff --git a/tsconfig.json b/tsconfig.json
index 5f7b294..4a611ef 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,8 @@
     "noEmit": true,
     "strict": true,
     "isolatedModules": true,
-    "esModuleInterop": true
+    "esModuleInterop": true,
+    "jsx": "react"
   },
   "include": [
     "src"

React の型定義ファイルをインストールする。

$ yarn add -D @types/react @types/react-dom

これで、$ yarn tscは通るにようになった。
だが Babel の対応が終わっていないので、ビルドは出来ない。

@babel/preset-reactをインストールしてbabel.config.jsを追加すれば、ビルドできるようになる。

$ yarn add -D @babel/preset-react
diff --git a/babel.config.js b/babel.config.js
index 27ad3e1..14bec9b 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -9,6 +9,12 @@ const presets = [
     },
   ],
   ['@babel/preset-typescript'],
+  [
+    '@babel/preset-react',
+    {
+      development: process.env.NODE_ENV === 'development',
+    },
+  ],
 ];
 
 const config = {

$ yarn buildしたあとに$ open index.htmlするとページが開き、Hello!と表示されているはず。

import/export

通常、React のコンポーネントは別ファイルに記述してそれをexport/importするのが一般的なので、ここでもそれに合わせる。

src/App.tsxとして、別ファイルに切り出す。

import React from 'react';

const App = () => <div>Hello!</div>;
export default App;
diff --git a/src/index.tsx b/src/index.tsx
index c31e3ed..3da2e7a 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
-const App = () => <div>Hello!</div>;
+import App from './App';
 
 ReactDOM.render(
   <App />,

だがこれでビルドしようとするとModule not found: Error: Can't resolve './App'となり、App.tsxをインポートできないので、webpack.config.jsを編集する。

diff --git a/webpack.config.js b/webpack.config.js
index be31412..219f7d9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -8,6 +8,9 @@ module.exports = {
     filename: '[name].js',
     path: path.resolve(__dirname, 'dest'),
   },
+  resolve: {
+    extensions: ['.ts', '.js', '.tsx', 'jsx'],
+  },
   module: {
     rules: [
       {test: /\.*(ts|tsx)$/, use: ['babel-loader']}

これでビルドできる。

Babel プラグインの利用

最後に、Babel プラグインの利用を試す。
サンプルとして利用するプラグインは、babel-plugin-react-remove-properties

github.com

React の要素から、任意の属性を取り除くことが出来る。
例えばdata-testは、デフォルトで取り除いてくれる。

テストコードのためにdata-testという属性を持たせたいが、プロダクションではこの属性は不要なので削除したい、というケースなどで使う。

早速、属性を追加する。

diff --git a/src/App.tsx b/src/App.tsx
index de582d9..38cd448 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,4 @@
 import React from 'react';
 
-const App = () => <div>Hello!</div>;
+const App = () => <div data-test="text">Hello!</div>;
 export default App;

これをビルドすると当然、そのままdata-testがレンダリングされてしまう。

<div id="app"><div data-test="text">Hello!</div></div>

babel-plugin-react-remove-propertiesを使えばこれを除去できる。

$ yarn add -D babel-plugin-react-remove-properties
diff --git a/babel.config.js b/babel.config.js
index 14bec9b..1175e1f 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -17,8 +17,15 @@ const presets = [
   ],
 ];
 
+const plugins = [];
+
+if (process.env.NODE_ENV !== 'test') {
+  plugins.push('react-remove-properties');
+}
+
 const config = {
   presets,
+  plugins,
 };
 
 module.exports = config;

これでビルドしたものをブラウザで確認すると、data-testが除去されているのが分かる。

<div id="app"><div>Hello!</div></div>

参考資料