クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【TypeScript】おみくじJSをTypeScriptに移植:型付け→モジュール分割(エラーあり)

メタ言語の続きです。前回はfsモジュールを使ってEJSファイルでJSONファイルのデータを読み込みました。今回からはTypeScriptのモジュール分割編に入ります。以前作った「おみくじJS」をTypeScriptに移植し、モジュールファイルに分割してみます。それではいきましょう!

【目次】

※参考:前回記事
【EJS】fsモジュールを使ってコンテンツのJSONデータを読み込む - クモのようにコツコツと

※参考:【メタ言語】HTMLテンプレートエンジン、AltCSS、AltJSのまとめ
qiita.com

前回のおさらい

前回まで作ったものこちら。 f:id:idr_zz:20200625181147j:plain

Sass(SCSS)とEJSをモジュール分割。さらにEJSはJSONファイルのデータから読み込んでテンプレートとデータの分離。

詳細はこちら

※参考:前回記事
【EJS】fsモジュールを使ってコンテンツのJSONデータを読み込む - クモのようにコツコツと

今回はTypeScriptのモジュール分割をやってみたい。

前回までのTypeScript

一番下の「ここ押せワンワン」ボタンを押すとアラートが出る。 f:id:idr_zz:20200625181346j:plain

ファイルは「script.ts」一つのみ。 f:id:idr_zz:20200625181440j:plain

「script.ts」のJSコード

document.addEventListener("DOMContentLoaded", () => {

    /* アロー関数 */
    const hellow = () => {
        window.alert('hellow TypeScript??????????');
        window.alert('こんにちは、型スクリプト');
        window.alert(title + 'は第' + num + '回です。');
        window.alert('おしまい');
    }

    /* 変数(型付け) */
    // let btn: Object;
    let btn = document.querySelector('.inner__text-btn');

    let title: string;
    title = 'JS編';

    let num: number;
    num = 3;

    /* イベント */
    btn!.addEventListener('click', hellow, false);

}, false);

詳細はこちちら

※参考:【gulp】Sass(SCSS)とTypeScriptを同時にコンパイルする環境を作る - クモのようにコツコツと

以前作ったおみくじJS

以前、Math.random()を使って作ってみたおみくじ。

See the Pen Omikuji by イイダリョウ (@i_ryo) on CodePen.

「引く」ボタンを押すとランダムなおみくじ結果が出る。

JSコード

//要素取得
const omikuji = document.getElementById("omikuji");

//運気
const unki = [ 
  "大吉",
  "吉",
  "中吉",
  "小吉",
  "末吉",
  "凶",
  "大凶"
]

//結果
function kekka(){
const nmb = Math.floor(Math.random() * unki.length );
omikuji.innerHTML = unki[nmb]; 
}

//イベント
omikuji.addEventListener("click", kekka, false);

おみくじJSの詳細はこちら

※参考:【JS】Math.random()でつくるサイコロとおみくじ - クモのようにコツコツと

今回これをTypeScriptに移植して、さらにモジュール分割もしてみたい。

JSONデータのテキストを修正

まず画面に表示しているテキストを修正したい。

テキストのデータは「data.json」にある。これがEJSに読み込まれている。

            {
            "title":"TypeScriptで書いたクリックイベント",
            "text": "<button class='inner__text-btn'>ここ押せワンワン</button>"
            }

テキストを打ち替える。

            {
            "title":"TypeScriptで作ったおみくじ",
            "text": "<button class='inner__text-btn'>おみくじを引く!</button>"
            }

gulpを起動する

npx gulp

ブラウザが立ち上がると…
f:id:idr_zz:20200625183532j:plain よっしゃ、テキストが打ち変わっている♪

EJSのJSONデータ読み込みの詳細はこちら

※参考:【EJS】fsモジュールを使ってコンテンツのJSONデータを読み込む - クモのようにコツコツと

おみくじJSのコードを移植

次におみくじJSのコードを移植する。

//運気
const unki = [ 
    "大吉",
    "吉",
    "中吉",
    "小吉",
    "末吉",
    "凶",
    "大凶"
];

//結果
function kekka(){
const nmb = Math.floor(Math.random() * unki.length );
alert(unki[nmb]); 
}

document.addEventListener("DOMContentLoaded", () => {

    /* 変数(型付け) */
    // let btn: Object;
    let btn = document.querySelector('.inner__text-btn');

    /* イベント */
    btn!.addEventListener('click', kekka, false);

}, false);

前半はおみくじJSからの移植。後半のクリックイベントにkekka()関数を紐づけている。

おみくじボタンを押すと… f:id:idr_zz:20200625185454j:plain よっしゃ!おみくじ結果が表示された!

ネイティブJSコードで書いてもそのまんま動くのがTypeScriptのいいところ♪*1

ボタンのSass(SCSS)を修正

ボタンを押すと青い枠線が出るのが気になったので修正したい。

ボタンのスタイルは「_inner.scss」モジュールにある。.inner-btnセレクタ。

.inner {
      /* 中略 */
      &-btn {
        background: $base-color;
        color: $text-color_w;
        padding: 10px;
      }  
    }
  }

ここにスタイルを追記する。

.inner {
      /* 中略 */
      &-btn {
        background: $base-color;
        color: $text-color_w;
        padding: 10px;
        border: none;
        border-radius: 5px;
        &:hover {
          cursor: pointer;
          opacity: 0.8;
        }
        &:focus {
          outline: none;
      }  
    }
  }
  • borderのスタイルをなくし、border-radiusで角丸も付ける
  • 擬似クラス:hoverでカーソルを指アイコン、不透明度も付ける
  • 擬似クラス:focusoutlineで青線を無しに

擬似クラス:focusoutline:プロパティの値を0noneに変えると青線が消える。

※参考:[Chrome] ボタン要素の周りに表示される青線を消す - Qiita

青線が出なくなった!! f:id:idr_zz:20200626052218j:plain

Sass(SCSS)のモジュール分割の詳細はこちら

※参考:【Sass(SCSS)】@importでBEMのBlockごとにファイル分割する - クモのようにコツコツと

※参考:【Sass(SCSS)】変数($)、@mixinを使ってモジュールを超えた共通スタイルを設定する - クモのようにコツコツと

おみくじJSに型を付ける

せっかくなのでおみくじJSにTypeScriptの型を付けてみる。

// 運勢
const unsei: string[] = [ 
    "大吉",
    "吉",
    "中吉",
    "小吉",
    "末吉",
    "凶",
    "大凶"
];

// 結果
let kekka = (): string => {
    let nmb: number = Math.floor(Math.random() * unsei.length );
    return unsei[nmb]; 
}

// おみくじ
let omikuji = (): void => {
    let massage: string = 'あなたの運勢は「' + kekka() + '」です。';
    alert(massage);
}

// イベント実行
document.addEventListener("DOMContentLoaded", () => {
    let btn: HTMLButtonElement = document.querySelector('.inner__text-btn') as HTMLButtonElement;
    btn!.addEventListener('click', omikuji, false);
}, false);

関数の構成を少し変えた。*2

  • kekka()関数からアラートの部分をomikuji()関数に分離
  • kekka()returnでテキストを返す
  • omikuji()の変数massageでメッセージ作成の処理を追加(この中でkekka()を実行)
  • クリックイベントはkekkaではなくomikujiを実行

TypeScriptの型を付けてみた。

  • 変数unseistring[]
  • アロー関数kekkastring
  • 変数nmbnumber
  • アロー関数omikujivoid
  • 変数massagestring
  • DOM要素btnHTMLButtonElement(キャストも付ける)

型の種類は下記を参考にした。処理のみで戻り値returnがない関数はvoid型になる。

※参考:TypeScriptの型システム - TypeScript Deep Dive 日本語版

※参考:TypeScriptの型入門 - Qiita

DOM要素は種類によって「HTMLElement」を付ける(ボタンはHTMLButtonElementなど)。

※参考:TypeScriptでDOM要素を作成する | 株式会社CONFRAGE ITソリューション事業部

また、DOMには「キャスト」を書く必要があるみたい。

※参考:TypeScript - TypeScriptで型に関するエラーが解消できない。|teratail

「キャスト」とは型を変換すること。

型変換 - Wikipedia

キャストは値部分に追記する。2つの方法がある。

  • 山カッコ< >で囲う
  • asで繋げる

山カッコはReactのJSXとカブるためasが推奨とのこと。

※参考:Type Assertion(型アサーション) - TypeScript Deep Dive 日本語版

型付きTypeScriptのコンパイル結果

ブラウザの動作。アラートはちゃんと出てる! f:id:idr_zz:20200626070949j:plain

コンパイル後の「script.js」

// 運勢
var unsei = [
    "大吉",
    "吉",
    "中吉",
    "小吉",
    "末吉",
    "凶",
    "大凶"
];
// 結果
var kekka = function () {
    var nmb = Math.floor(Math.random() * unsei.length);
    return unsei[nmb];
};
// おみくじ
var omikuji = function () {
    var massage = 'あなたの運勢は「' + kekka() + '」です。';
    alert(massage);
};
// イベント実行
document.addEventListener("DOMContentLoaded", function () {
    var btn = document.querySelector('.inner__text-btn');
    btn.addEventListener('click', omikuji, false);
}, false);

型などは全てなくなってES5の書き方になっている。

omikuji()関数をモジュール分割

さて、いよいよここからomikuji()関数をモジュール分割してみたい。

分割方法は「CommonJS」「AMD」などいろいろあるが基本的には「ESモジュール」でいいようだ。

※参考:ファイルモジュールの詳細 - TypeScript Deep Dive 日本語版

ESモジュールはこちらを参照。Vue.jsやReactのコンポーネントでもお馴染みな書き方。

※参考:【JS】モジュール(import / export)でどんなことができるのか事始めてみた - クモのようにコツコツと

まず「omikuji」フォルダとその中に「omikuji.ts」を作る。 f:id:idr_zz:20200627043845j:plain

「omikuji.ts」のコード

// 運勢
const unsei: string[] = [ 
    "大吉",
    "吉",
    "中吉",
    "小吉",
    "末吉",
    "凶",
    "大凶"
];

// 結果
let kekka = (): string => {
    let nmb: number = Math.floor(Math.random() * unsei.length );
    return unsei[nmb]; 
}

// おみくじ
export let omikuji = (): void => {
    let massage: string = 'あなたの運勢は「' + kekka() + '」です。';
    alert(massage);
}

「script.ts」のイベント以外を持ってくる。omikuji()にはexportを付ける。

defaultインポートはimport側で自由に関数名を付けれる。しかしそれは混乱の元にもなるため、あまりお勧めしない方法のようだ。

※参考:TypeScriptでモジュールを作成する/インポートする (export, import)|まくろぐ

「script.ts」のコード

import { omikuji } from "./omikuji/omikuji";

// イベント実行
document.addEventListener("DOMContentLoaded", () => {
    let btn: HTMLButtonElement = document.querySelector('.inner__text-btn') as HTMLButtonElement;
    btn!.addEventListener('click', omikuji, false);
}, false);

冒頭で「omikuji.ts」をインポート。関数名omikujiを読み込む。

コンパイル結果…エラーになった。。

コンパイル後のscript.js

"use strict";
exports.__esModule = true;
var omikuji_1 = require("./omikuji/omikuji");
// イベント実行
document.addEventListener("DOMContentLoaded", function () {
    var btn = document.querySelector('.inner__text-btn');
    btn.addEventListener('click', omikuji_1.omikuji, false);
}, false);
  • exports.__esModule、これはなんだろう??
  • インポート部分はimport()(ESモジュール)からrequire()(CommonJS)に変換されている

TypeScriptは「omikuji.ts」のコード自体がインポートされるようではないようだ*3

そして「omikuji」フォルダや「omikuji.js」もコンパイルはされていない。 f:id:idr_zz:20200627044001j:plain
なので当然「omikuji関数が見つからない」になると思われる。

コンソールを見ると下記のエラー

Uncaught ReferenceError: exports is not defined

キャッチされていないReferenceError:エクスポートが定義されていません

ふむ、omikuji以前に冒頭のexports.__esModule = true部分がエラーになっている。

exportsで検索かけても他のファイルには出てこない名前なんだよなー。

「おみくじ」ボタンを押しても… f:id:idr_zz:20200627044431j:plain アラートは出なくなった。。

最後に

今回はここまでとします!

※わかったこと:

  • TypeScriptはJSコードをそのまま書いても動く(後方互換)
  • JSからTypeScriptに移植するにあたり、型の付け方に詳しくなった
  • モジュールのインポートはSass(SCSS)やEJSのようにコードごとインポートされるわけではなさそう

次回は、今回発生したエラーの検証と対策にトライします。

エラーが解消したらomikujiモジュールの中のパーツもさらにモジュール化したいです。

それではまた!


※参考:【メタ言語】HTMLテンプレートエンジン、AltCSS、AltJSのまとめ
qiita.com

*1:EJSとSass(SCSS)お同じく後方互換なメタ言語のため移行がしやすい

*2:これでkekka()がアラート以外の用途にも再利用できる。メッセージは一文字(「吉」とか)だと寂しかったので文章にした

*3:Sass(SCSS)やEJSはエクスポート元のコード自体がインポートされた