WebアプリケーションのJavaScriptをTypeScriptに置き換えたい

## 前提

既存のWebシステムがあり，工数の都合やシステムの制約からHTML(JSPやThymeleaf)はそのままで，JavaScriptだけTypeScriptに置き換えたいことがあると思います．

既存のWebシステムとなるとjQueryがよく利用されていると思いますので，本投稿ではWebアプリケーションでよくある操作をTypeScriptに書き換えることを目的としています．
ReactやVueなどは使いません．

## 環境構築+初期設定

環境構築は本題とはあまり関係ないので，こういう環境で実施しましたという紹介だけです．
すでに環境はできている方が多いと思いますので読み飛ばしてください．

まずは必要なパッケージのインストールです．

```sh
# 初期設定
npm init #Enter連打
# 必要パッケージのインストール
npm install -D typescript jquery @types/jquery
# TypeScriptの初期設定
npx tsc --init
```

動作確認のためのサンプルページを作成します．

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>テストページ</title>
  </head>
  <body>
    <script
      src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
      integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
      crossorigin="anonymous"
    ></script>
    <script src="app.js"></script>
  </body>
</html>
```

```ts:app.ts
$(function () {
    $(document.body).html("hello");
});
```

サンプルページが作成できたらTypeScriptをコンパイルします．

```sh
npx tsc app.ts
```

app.jsが生成されたら，index.htmlをブラウザで開き，`hello`が表示されていれば準備完了です．

# 各種置き換えについて

ここからは置き換え方法について記載していきます．

## 変数，定数，関数を定義したい

よくチュートリアルで出てくる単純な足し算です．

```js:元のJavaScript
var a = 10;
var b = 40;
function sum(a, b) {
    return a + b;
}
console.log(sum(a, b));
```

これをTypeScriptに書き換えると以下になります．

```ts:書き換えた後のTypeScript
let a = 10;
let b = 40;
function sum(a: number, b: number): number {
  return a + b;
}
console.log(sum(a, b));
```

ぱっと見て`:number`という型の宣言が増えています．
これがあることで，sumを数値以外で使おうとしたらコンパイルエラーになったり，returnを忘れているとコンパイルエラーになったりと実行前から色々チェックしてくれます．
変数については`let`を利用していますが，定数の場合は`const`を利用してください．

## 要素を取得したい

このセクションはjQueryを使う場合はJavaScriptもTypeScriptも同じです．

### テキスト

```html:HTML
<input id="text" type="text" value="ねこ" />
```

```js:元のJavaScript
console.log($('#text').val()); // ねこ
```

```ts:書き換えた後のTypeScript
// jQuery
console.log($('#text').val());

// non jQuery
console.log((document.getElementById('text') as HTMLInputElement)?.value);
```

jQueryを使っている場合は元コードと変わりません．
jQueryを使わない場合はHTMLInputElement型へのアサーションが必要になります．

### チェックボックス

```html:HTML
<input type="checkbox" name="check" value="ねこ" />ねこ
<input type="checkbox" name="check" value="かわいい" />かわいい
<input type="checkbox" name="check" value="にゃー" />にゃー
```

```js:元のJavaScript
$('input[name=check]').each(function () {
    // 値の取得
    console.log($(this).val()); // ねこ
    // チェック状態の取得
    console.log($(this).prop('checked')); // true
});
```

```ts:書き換えた後のTypeScript
// jQuery + アロー演算子
$('input[name=check]').each((_index, element) => {
  // 値の取得
  console.log($(element).val());
  // チェック状態の取得
  console.log($(element).prop('checked'));
});

// non jQuery
let checklist = document.getElementsByName('check');
checklist.forEach((element) => {
  let input = element as HTMLInputElement;
  // 値の取得
  console.log(input.checked);
  // チェック状態の取得
  console.log(input.value);
});
```

宗教上の理由から(モダンなコードを書いている感を醸し出すために)アロー演算子を使って書きましたが，アロー演算子を使用しない場合は，元のJavaScriptと同じ書き方です．
jQueryを使わない場合はいつものように(?)HTMLInputElement型へのアサーションが必要になります．

### セレクトボックス

```html:HTML
<select id="selectBox" name="select">
  <option value="1">いぬ</option>
  <option value="2">さる</option>
  <option value="3">きじ</option>
</select>
```

```js:元のJavaScript
console.log($('#selectBox').val()); // 値の取得 例:1
console.log($('#selectBox option:selected').text()); // テキストの取得 例:いぬ
```

```ts:書き換えた後のTypeScript
// jQuery
console.log($('#selectBox').val());
console.log($('#selectBox option:selected').text());

// non jQuery
let select = document.getElementById('selectBox') as HTMLSelectElement;
console.log(select.value);
console.log(select.options[select.selectedIndex].text);
```

jQueryはTypeScriptでもそのままです．
jQueryを使わない場合はHTMLSelectElement型へのアサーションが必要です．


## イベントを実装したい

ボタンをクリックしたら，チェックボックスにチェックを入れたら，Windowをリサイズしたらなどの条件でイベントを登録している場合の書き方です．

```html:ボタンの定義
<button id="testButton" value="1">ボタン</button>
```

```js:元のJavaScript
$('#testButton').on('click', function () {
    console.log(this.value);
});
```

```ts:書き換えた後のTypeScript
// jQuery
$('#testButton').on('click', function () {
  console.log((this as HTMLButtonElement).value);
});

// non jQuery
document.getElementById('testButton')?.addEventListener('click', function () {
  console.log((this as HTMLButtonElement).value);
});
```

作成したボタンのvalueを取得するためにはHTMLElement型のthisをHTMLButtonElement型にアサーション(キャスト)する必要があります．
※ 作成したい要素に応じて型を変更してください

脊髄反射で`function`ではなくアロー演算子`()=>`を使った場合は，thisがglobalThisになるため，値が取れなくなります．(コンパイルエラーになります．)
クリックした要素を参照する必要がなければアロー演算子でも問題ありません．

宗教上の理由でアロー演算子を使用しないといけない場合は，以下のように記述できます．

```ts:TypeScript(アロー演算子版)
// jQuery
$('#testButton').on('click', (event: JQuery.ClickEvent) => {
  console.log(event.target.value); // ※targetはany型
});

// non jQuery
document.getElementById('testButton')?.addEventListener('click', (event: MouseEvent) => {
    console.log((event.target as HTMLButtonElement).value);
})
```

## 親画面の関数を実行したい

※ サーバ上に配置しないと動作しません．

```js:元のJavaScript
// 親画面
function setUser(userName){
  console.log(userName);
}

// 子画面
window.opener.setUser('たま');
```

```ts:書き換えた後のTypeScript
// 親画面
function setUser(userName: string) {
  console.log(userName);
}

// 子画面 
window.opener?.setUser('たま');
```

ほとんど変わっていません．ほとんどそのまま利用できますが，注意点が２つあります．

- openerはany型であるため，setUserという関数が存在するか不明で引数のチェックなども放棄される
- webpackなどのモジュールバンドラを使うと親画面のsetUserがクロージャの中に閉じ込められるので，子画面から参照できない

## webpackを使っていても親画面の関数を実行したい

webpackの説明については割愛します．

```ts
// 親画面
interface ParentWindow extends Window{
  setUser(userName: string): void;
}

(window as ParentWindow & typeof globalThis).setUser = (userName: string) => {
  console.log(userName);
};

// 子画面 
(window.opener as ParentWindow)?.setUser('たま');
```

すでに定義されているWindowを拡張してsetUserを追加します．
そのあとで親画面でsetUserを実装します．

子画面から呼び出すときはopenerがany型なのでWindow型にアサーションしてからsetUserを呼び出します．
これで親画面での関数定義と子画面での呼び出しで型チェックできるようになります．
ただし，親画面でのsetUserを定義し忘れることは検知できません．

※ tsconfigのlibで`["DOM"]`を定義しているか，モジュール形式かどうかで定義方法が変わってきます．すべて解説できるほど知識がないのでここまでとしておきます．

## まとめ

間違いなどあればご指摘いただけると幸いです．
jQuery使っていると型変換しなくても良いので楽