ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

TypeScriptの開発環境構築メモ

TypeScriptのインストール

TypeScriptのインストール方法はグローバルインストールとローカルインストールの2種類あります。

グローバルインストール方法は以下です。

$ npm install -g typescript

環境を汚したくない場合はローカルインストールをします。node_modulesが作成されその中にインストールされます。こっちだとpackage.jsonpackage-lock.jsonにTypeScript情報が追記されて、移植性が高いのでおすすめ。

$ npm install --save typescript
# または
$ npm install --save-dev typescript

--saveと--save-devの違いはnpmの--save, --save-dev, --save-optionalの違い - how to code something を参照。今回私はChrome拡張を作るつもりなので--save-devでインストールします。

ローカルインストールの場合、tscコマンドを相対パスで指定する必要があります。毎回それだと面倒くさいので、package.json(package-lock.jsonではない)のscriptsフィールドにショートカットを記述します。

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "tsc": "tsc"
  },
  ...
}

こうすることで、npm runコマンドでローカルインストールしたnode_modules内のtscを実行できるようになります。

$ tsc  # グローバルインストールの場合
$ npm run tsc  # ローカルインストールの場合

参考URL

tsconfig.jsonの作成

次にtsconfig.jsonの雛形を作成します。これはTypeScriptからJavaScriptにトランスパイルする際の設定ファイルです。

# グローバルインストールの場合
$ tsc --init        

# ローカルインストールの場合
$ ./node_modules/.bin/tsc --init

# npm run tsc はオプション引数を渡すことができないので、以下だとエラーが発生する
$ npm run tsc --init  

tsconfig.jsonの代表的なプロパティは以下のような感じです。

compilerOptions.target

生成するJavaScriptECMAScriptバージョン。

compilerOptions.module

モジュールの形式。

compilerOptions.strict

厳密な型変換をするかどうか(暗黙的な型変換を許さないかどうか)。TrueでOK

compilerOptions.esModuleInterop

CommonJS形式で書かれた外部ライブラリのモジュールを妥当に扱えるようにするらしいです。よくわからないからそのままで。

compilerOptions.outDir

tscをしてコンパイルしたとき、出力ファイルはtsconfig.jsと同じ場所に出力されます。別フォルダに出力したいときはこのプロパティで指定します。

dstフォルダに出力したいときは以下のようにします。

{
  "compilerOptions": {
    "outDir": "dst",                        /* Redirect output structure to the directory. */
  ...
}

Include

tscコマンドでコンパイルするファイルをフォルダで指定します。

srcフォルダ内のファイルを対象としたいときは以下のようにします。

{
  "compilerOptions": {
  ...
  },
  "include": [
    "src/**/*"
  ]
}

allowJsとcheckJs

tscコンパイル対象にJavaScriptファイルを含めたいときには、これらの項目を設定します。既存のJavaScriptファイルをTypeScript内でimportしたいときはこの設定が必要です。

{
  "compilerOptions": {
    ...,
    "allowJs": true,
    "checkJs": true
  }
}

まとめ

tsconfig.jsonは、例えば私の場合は以下のようになりました。

{
  "compilerOptions": {
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "allowJs": true,                       /* Allow javascript files to be compiled. */
    "checkJs": true,
    "outDir": "dst",                        /* Redirect output structure to the directory. */
    "strict": true,                           /* Enable all strict type-checking options. */
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
  },
  "include": [
    "src/**/*"
  ]
}

ビルドツールのインストール

実際のアプリ開発では直接tscコマンドを打つことはなく、ビルドツールと連携して使うことが多いようです。

私の場合はwebpackを使いたいのでそれに関するものをインストールします。

webpackのインストール

webpackはTypeScriptの自動コンパイル、複数のソースコードの結合、ソースコード更新時の自動リロードなどをしてくれます。一部tscと機能が被っていますね。

$ npm install --save-dev typescript ts-loader webpack webpack-cli webpack-dev-server webpack-merge

ざっくりとですが、各モジュールについて私の理解で書いておきます。

  • typescript
    • TypeScriptファイル(.ts)をコンパイルするために必要。
  • ts-loader
    • よくわからないけど、TypeScriptと連携するために必要なwebpack用のloader。
  • webpack
    • webpackを使って.tsファイルをコンパイルするために必要。
  • webpack-cli
    • 開発中に、ファイルを保存したらコンパイルも自動でされるみたいな便利なことをするために必要。
  • webpack-dev-server
    • 開発用WEBサーバ。
  • webpack-merge
    • webpack.config.jsを、開発用と本番用にファイルを分割するために必要。

ts-loaderはnode_modules内にtypescriptがあることを前提としているので、typescriptをローカルインストールする必要があるらしいです。

webpack.config.js

webpackの設定ファイルです。設定方法は参考URLのTypeScriptチュートリアル① -環境構築編- - Qiitaが詳しいのでそちらを参照。

const path = require('path');
module.exports = {
    entry: {
        content_scripts: './src/content_scripts.ts'
    },  
    output: {
        path: path.join(__dirname,'dst'),
        filename: '[name].js'  // [name]は、entryのプロパティ名(content_scripts)
    },
    optimization: {
        minimize: true
    },
    resolve: {
        extensions:['.ts','.js']
    },
    devServer: {
        contentBase: path.join(__dirname,'dst')
    },
    module: {
        rules: [
            {
                test:/\.ts$/,
                use: {
                    loader:'ts-loader'
                }
            }
        ]
    }
}

出力フォルダの指定などがtsconfig.jsonと被ってますね。webpackを通じてコンパイルするときはwebpack.config.jsの設定でされるのできちんと設定しておきます。

webpack.config.jsを開発用と本番用にファイルを分ける

webpackで開発用/本番用の設定を分ける - Qiitaが詳しいのでそちらを参照。

例えば、開発用は出力ファイルを圧縮せずに、本番用は出力ファイルを圧縮する。といった切り替えをしたいです。このようにしたいとき、公式の推奨によるとwebpack.config.jsを以下のようにファイル分割します。

  • webpack.common.js
    • 共通設定。開発用と本番用の両方に適用したい設定を記述します。
  • webpack.dev.js
    • 開発用設定
  • webpack.prod.js
    • 本番用設定

例えば私は以下のようになりました。

webpack.common.js

const path = require('path');
module.exports = {
    entry: {  // ビルドの起点となるファイルの指定
        content_scripts: './src/content_scripts.ts'
    },
    output: {  // ビルド結果の出力場所
        path: path.join(__dirname,'dst'),
        filename: '[name].js'  // [name]は、entryのプロパティ名(content_scripts)
    },    
    resolve: {  // モジュールとして扱いたいファイルの拡張子を指定する
        extensions:['.ts','.js']
    },
    devServer: {
        // webpack-dev-serverの公開フォルダ
        contentBase: path.join(__dirname,'dst')
    },
    module: {
        rules: [
            {
                // 拡張子が.tsで終わるファイルに対して、TypeScriptコンパイラを適用する
                test:/\.ts$/,
                use: {
                    loader:'ts-loader'
                }
            }
        ]
    }
}

webpack.dev.js

// webpack-merge ver.5.0.3以降は { merge } = ... という書き方になる
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js') // 汎用設定をインポート

// common設定とマージする
module.exports = merge(common, {
    mode: 'development', // 開発モード
    devtool: 'inline-source-map', // 開発用ソースマップ
    optimization: {
        minimize: false  // 出力JSファイルを圧縮しない
    }
})

webpack.prod.js

// webpack-merge ver.5.0.3以降は { merge } = ... という書き方になる
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js') // 汎用設定をインポート

// common設定とマージする
module.exports = merge(common, {
    mode: 'production', // 本番モード
    optimization: {
        minimize: true  // 出力JSファイルを圧縮する
    }
})

package.jsonを編集してwebpackのコマンドを簡単に使えるようにする

package.jsonのscriptsフィールドに、以下のプロパティを追加します。

  "scripts": {
    "tsc": "tsc",
    "build": "webpack --config webpack.prod.js",
    "build-dev": "webpack-cli -w --config webpack.dev.js",
    "server": "webpack-dev-server --config webpack.dev.js"
  },

これにより、

  • npm run buildで、webpackをProduction(本番環境)モードで起動して、ビルドする
    • .tsファイルを本番用にビルドする用のコマンド
  • npm run build-devで、webpack-cliをDevelopment(開発)モードかつwatchモード(ファイルを保存すると自動ビルドされる)で起動する
    • 普段の開発中はこちらを使う
  • npm run serverで、webpack-dev-serverを開発モードで起動する
    • webpack-dev-serverは、開発用WEBサーバーを起動するコマンド。localhost:8080にアクセスできる

ができるようになりました。webpack-dev-serverはビルド処理もしてくれるため、基本的にnpm run serverだけ使っておけば問題ないらしいですが、webpack-dev-serverは、ファイルを変更したときはdstフォルダに出力ファイルは更新されず、メモリ上に保存されるようです。

私の場合はChrome拡張を作りたかったので、Chrome拡張は出力ファイルをChromeにアップロードする必要があるので、出力ファイルが更新されないと困ります。なので代わりにwebpack-cliを使い、npm run build-devを中心に使って開発を進めることになります。

参考URL

@typesのインストール

@typesは型定義ファイルで、例えばChrome拡張を作るときは@types/chromeをインストールしておかないと、chrome.runtime.onMessage.addListenerなどをコードに書いてコンパイルしたらTS2304: Cannot find name 'chrome'.みたいなコンパイルエラーが出ます。なのでプロジェクトに必要な型定義ファイルを適宜インストールする必要があります。

例えばChrome拡張の場合は、@types/chromeをインストールします。

$ npm install --save-dev @types/chrome

Reactのインストール

今回、Chrome拡張で設定画面を作りたいのですが、せっかくなので練習も兼ねてReactで作ることにしました。

$ npm install --save-dev react react-dom
$ npm install --save-dev @types/react-dom @types/react-dom

「実践TypeScript」という本にはparcelを使うと便利とありますが、私の場合はwebpackを使用していますので、今回はparcelは不要なのでインストールしません。parcelは設定ファイルなしで即React環境を構築できるのがメリットな反面、複雑な設定ができないというデメリットがあるようです。

Reactを使う場合、.tsxファイルを使いますので、これのビルドを許可するように設定ファイルを書き換えます。

tsconfig.json

  "compilerOptions": {
    "target": "ES5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "es2015",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "jsx": "react",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
   ...

jsxを'react'にします。また、必須ではないですがmoduleをes2015にしました。理由は 最新版TypeScript+webpack 5の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き) - ICS MEDIA の「TypeScriptの設定ファイル: tsconfig.json」の章を読んでそうしました。

webpack.common.js

    entry: {
        content_scripts: './src/content_scripts.ts',
        background: './src/background.ts',
        index: './src/index.ts',
        app:'./src/react/app.tsx'  // ★これを追加
    },
    output: {
        // モジュールバンドルを行った結果を出力する場所やファイル名の指定
        // "__dirname"はこのファイルが存在するディレクトリを表すnode.jsで定義済みの定数
        path: path.join(__dirname,'dst/js'),
        filename: '[name].js'  // [name]は、entryのプロパティ名(content_scripts)
    },
    // モジュールとして扱いたいファイルの拡張子を指定する
    // 例えば「import Foo from './foo'」という記述に対して"foo.ts"という名前のファイルをモジュールとして探す
    // デフォルトは['.js', '.json']
    resolve: {
        extensions:['.ts','.tsx', '.js']  // ★tsxを追加
    },
    devServer: {
        // webpack-dev-serverの公開フォルダ
        contentBase: path.join(__dirname,'dst')
    },
    // モジュールに適用するルールの設定(ここではローダーの設定を行う事が多い)
    module: {
        rules: [
            {
                // 拡張子が.tsで終わるファイルに対して、TypeScriptコンパイラを適用する
                test:/\.(ts|tsx)$/,  // ★tsxを追加
                use: {
                    loader:'ts-loader'
                }
            }
        ]
    }

フォルダ構成は次のようになっています(一部省略しています)。

root/
├dst/
│  ├js/
│  │  └ app.js
│  └ index.html
├src/
│  └ react/
│      └ app.tsx
├package.json
├tsconfig.json
├ webpack.common.js

ReactでDOMを構築するためのファイルはsrc/react/app.tsxです。これをnpm run build-test等でコンパイルすると、dst/js/app.jsが作成されます。index.htmldstフォルダ側に作成し、app.jsを読み込むようにします。

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>ReactTest</title>
  </head>
  <body>
    <div id="app"></div>
    あああああああああああああああああああああああああああ
    <!-- Load our React component. -->
    <script src="/js/app.js"></script>
  </body>
</html>

app.tsx

import * as React from 'react';
import { render } from 'react-dom';

render(<div>Hello World!!!!!</div>, document.getElementById('app'));

コンパイル後にWebサーバーを立ち上げindex.htmlにアクセスし、「Hello World!!!!!」が表示されていたら成功です。

参考URL

index.htmlをsrcフォルダに入れたい

「index.htmlファイルがdstにあってそれを直接編集するのおかしくねぇ?srcにあるべきじゃん」と思っていて、そういう方法がないか調べました。

React & TypeScriptのプロジェクト作成 - TypeScript Deep Dive 日本語版にその方法が載っていたのでそれを踏襲します。

clean-webpack-pluginhtml-wabpack-pluginを使うので、それらをインストールします。

$ npm install --save-dev clean-webpack-plugin html-webpack-plugin

その後、webpack.common.js(webpack.config.js)を以下のように編集します(編集箇所を★でマークしています)。

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');  // ★追加
const HtmlWebpackPlugin = require('html-webpack-plugin');  // ★追加

module.exports = {
    // モジュールバンドルを行う起点となるファイルの指定
    // 指定できる値としては、ファイル名の文字列や、それを並べた配列やオブジェクト
    // 下記はオブジェクトとして指定した例
    entry: {
        content_scripts: './src/content_scripts.ts',
        background: './src/background.ts',
        index: './src/index.ts',
        app:'./src/react/app.tsx'
    },
    // ★pluginsプロパティを追加
    plugins: [
        new CleanWebpackPlugin({
            cleanAfterEveryBuildPatterns: ['dst']
        }),
        new HtmlWebpackPlugin({
            template: 'src/templates/index.html'
        }),
    ],
    output: {
        // モジュールバンドルを行った結果を出力する場所やファイル名の指定
        // "__dirname"はこのファイルが存在するディレクトリを表すnode.jsで定義済みの定数
        path: path.join(__dirname,'dst'),  // ★出力フォルダをdstに修正
        filename: 'js/[name].js'  // ★jsファイルの出力は 'js/[name].js'に修正
    },
    // モジュールとして扱いたいファイルの拡張子を指定する
    // 例えば「import Foo from './foo'」という記述に対して"foo.ts"という名前のファイルをモジュールとして探す
    // デフォルトは['.js', '.json']
    resolve: {
        extensions:['.ts','.tsx', '.js']
    },
    devServer: {
        // webpack-dev-serverの公開フォルダ
        contentBase: path.join(__dirname,'dst')
    },
    // モジュールに適用するルールの設定(ここではローダーの設定を行う事が多い)
    module: {
        rules: [
            {
                // 拡張子が.tsで終わるファイルに対して、TypeScriptコンパイラを適用する
                test:/\.(ts|tsx)$/,
                use: {
                    loader:'ts-loader'
                }
            }
        ]
    }
}

フォルダ構成は以下のようになっています(一部省略)。

root/
├dst/
│  └js/
│     └ app.js
├src/
│  ├ react/
│  │   └ app.tsx
│  └ templates/
│        └ index.html
├package.json
├tsconfig.json
├ webpack.common.js

これでnpm run build-test等してビルドすると、src/templates/index.htmlをビルドしたものがdst/直下に作成されます。

これで、ソースファイルはsrcフォルダにまとめることができました。

その他の参考

編集履歴

日時 編集内容
2022/1/9 webpack-mergeをver.5.0.3以降の書き方に修正
2020/3/19 tsconfig.jsonの作成方法を修正
2020/1/6 React公式の参考URLを追加
2019/12/31 「index.htmlをsrcフォルダに入れたい」の項目を追加
2019/12/29 Reactのインストールの項目を追加
2019/12/15 16:00頃 @typesに関する記述を追加
2019/12/15 04:41頃 初版