【React】JavaScriptからTypeScriptに移行してみた

# 概要

前回、「文字の類似度」からポケモン名をサジェストするコンポーネントを実装しました。

https://qiita.com/ishi720/items/a026b8b29b31e0ef6ee0

ただ、勢いで作ってしまったため、**JavaScriptベースのReactアプリ**で作ってしまいました。そのため、**TypeScriptベースのReact**に移行したいと思います。

# 移行作業

今回の移行方法としては、**TypeScriptベースのReact**で新規のアプリを新規で作成し、その上に追加開発分を引っ越しします。


## 1. 新しくTypeScriptベースのReactアプリを作成

下記コマンドで新しくTypeScriptベースのReactアプリを作成します。
名前は何でも良いです。

```bash
$ npx create-react-app new-app --template typescript
```

## 2. 追加のライブラリをインストール

今回のプロジェクトは、`sass`しか追加インストールしていないので直接コマンドをたたきました。

```bash
$ yarn add sass
```

## 3. 追加開発分ファイルの移行

旧プロジェクトから、新プロジェクトに追加開発分のファイルを持ってきます。拡張子がjsのファイルはtsxに書き換えます。

私のプロジェクトでは以下のファイルのみでした。

|旧プロジェクト|新プロジェクト|
|-|-|
|App.js|App.tsx|
|pokeNameList.js|pokeNameList.tsx|
|App.scss|App.scss|

## 4. JavaScriptをTypeScriptに書き換え

`pokeNameList.tsx`は、ただの配列が定義されているだけだったのでファイル名の修正だけで問題ありませんでした。

App.tsxについては、下記の変更を行いました。

https://github.com/ishi720/levenshtein_distance_search/commit/cc135ce60cd6147276d2f426d619c7dd634effce#diff-26ad4b834941d9b19ebf9db8082bd202aaf72ea0ddea85f5a8a0cb3c729cc6f2

一応、ファイルも記載しておきます。

<details>
<summary>移行前のApp.js</summary>

```js:App.js
import React, { useState, useEffect } from 'react';
import styles from './App.scss';
import { pokeNameList } from './pokeNameList';

const App = () => {
  return <SuggestForm />;
};

const SuggestForm = () => {
  const [value, setValue] = useState('');
  const suggestNum = 15 ;
  const wordList = pokeNameList;
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    const suggestions = getSuggest(hiraganaToKatakana(value), wordList, suggestNum);
    setSuggestions(suggestions);
  }, [value, wordList]);

  // イベント
  const handleChange = (event) => {
    const updatedValue = event.target.value;
    setValue(updatedValue);
  };
  /**
  * クリックされたワードを検索ボックスにセット
  * @param {word} str1 文字列
  */
  const handleClickWord = (word) => {
    setValue(word); 
  };
  /**
  * レーベンシュタイン距離の計測
  * @param {string} str1 文字列1
  * @param {string} str2 文字列2
  * @return {array} レーベンシュタイン距離の計測結果
  */
  const levenshteinDistance = (str1, str2) => {
    const len1 = str1.length;
    const len2 = str2.length;
    
    const dp = [];
    
    for (let i = 0; i <= len1; i++) {
      dp[i] = [];
      for (let j = 0; j <= len2; j++) {
        if (i === 0) {
          dp[i][j] = j;
        } else if (j === 0) {
          dp[i][j] = i;
        } else {
          dp[i][j] = 0;
        }
      }
    }
    
    for (let i = 1; i <= len1; i++) {
      for (let j = 1; j <= len2; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
        }
      }
    }
    
    return dp[len1][len2];
  };
  /**
  * 複数の文字列をレーベンシュタイン距離で評価した結果をソートし返却する
  * @param {string} searchWord 検索文字列
  * @param {array} wordList 検索対象の一覧
  * @param {number} n 返却する文字列の数
  * @return {array} 評価順のデータ
  */
  const getSuggest = (searchWord, wordList, n) => {
    return wordList
      .sort((a, b) => levenshteinDistance(searchWord, a) - levenshteinDistance(searchWord, b))
      .slice(0, n);
  };
  /**
  * ひらがなをカタカナに変換する
  * @param {string} s ひらがな文字列
  * @return {string} カタカナ文字列
  */
  const hiraganaToKatakana = (s) => {
    return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
      return String.fromCharCode(match.charCodeAt(0) + 0x60);
    });
  };

  return (
    <div>
      <div>
        <label name="pokemonSearch">ポケモン名　</label>
        <input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
      </div>
      <div id="wordList">
        <p>もしかして...</p>
        {suggestions.map((word, index) => (
          <span
            key={index}
            className="keyword"
            onClick={() => handleClickWord(word)}
          >
            {word}
          </span>
        ))}
      </div>
    </div>
  );
};

export default App;
```

</details>

<details>
<summary>移行後のApp.tsx</summary>

```ts:App.tsx
import React, { useState, useEffect } from 'react';
import './App.scss';
import { pokeNameList } from './pokeNameList';

const App = () => {
  return <SuggestForm />;
};

const SuggestForm = () => {
  const [value, setValue] = useState<string>('');
  const suggestNum: number = 15 ;
  const wordList: string[] = pokeNameList;
  const [suggestions, setSuggestions] = useState<string[]>([]);

  useEffect(() => {
    const suggestions: string[] = getSuggest(hiraganaToKatakana(value), wordList, suggestNum);
    setSuggestions(suggestions);
  }, [value, wordList, suggestNum]);

  // イベント
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const updatedValue = event.target.value;
    setValue(updatedValue);
  };
  const handleClickWord = (word: string) => {
    setValue(word); // クリックされたワードを検索ボックスにセットする
  };
  /**
  * レーベンシュタイン距離の計測
  * @param {string} str1 文字列1
  * @param {string} str2 文字列2
  * @return {array} レーベンシュタイン距離の計測結果
  */
  const levenshteinDistance = (str1: string, str2: string) => {
    const len1 = str1.length;
    const len2 = str2.length;
    
    const dp: number[][] = [];
    
    for (let i = 0; i <= len1; i++) {
      dp[i] = [];
      for (let j = 0; j <= len2; j++) {
        if (i === 0) {
          dp[i][j] = j;
        } else if (j === 0) {
          dp[i][j] = i;
        } else {
          dp[i][j] = 0;
        }
      }
    }
    
    for (let i = 1; i <= len1; i++) {
      for (let j = 1; j <= len2; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
        }
      }
    }
    
    return dp[len1][len2];
  };
  /**
  * 複数の文字列をレーベンシュタイン距離で評価した結果をソートし返却する
  * @param {string} searchWord 検索文字列
  * @param {array} wordList 検索対象の一覧
  * @param {number} n 返却する文字列の数
  * @return {array} 評価順のデータ
  */
  const getSuggest = (searchWord: string, wordList: string[], n: number) => {
    return wordList
      .sort((a, b) => levenshteinDistance(searchWord, a) - levenshteinDistance(searchWord, b))
      .slice(0, n);
  };
  /**
  * ひらがなをカタカナに変換する
  * @param {string} s ひらがな文字列
  * @return {string} カタカナ文字列
  */
  const hiraganaToKatakana = (s: string) => {
    return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
      return String.fromCharCode(match.charCodeAt(0) + 0x60);
    });
  };
  return (
    <div>
      <div>
        <label htmlFor="pokemonSearch">ポケモン名　</label>
        <input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
      </div>
      <div id="wordList">
        <p>もしかして...</p>
        {suggestions.map((word, index) => (
          <span
            key={index}
            className="keyword"
            onClick={() => handleClickWord(word)} 
          >
            {word}
          </span>
        ))}
      </div>
    </div>
  );
};

export default App;
```
</details>


## 5. 動作確認

```bash
$ yarn start
```

![Animation4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/c18ce0c8-a005-6beb-fe1d-8da6f7e25d16.gif)

問題なく動いていますね！

## 6. Gitへ変更内容をプッシュ

新プロジェクトのgitの紐づけが無い状態になっているので、旧プロジェクトのGitへ紐づけを行います。

- 新プロジェクト内の`.git`フォルダを削除
- 旧プロジェクト内の`.git`フォルダを新プロジェクトにコピーする
- 変更内容をコミットする（下記コマンド参照）
  ```bash
  $ git add --all
  $ git commit -m "JavaScript から TypeScriptへ移行"
  $ git push origin HEAD
  ```

# 終わりに

今回は、JavaScriptベースのReactアプリをTypeScriptベースのReactアプリに移行する方法をまとめました。

アプリの開発量が多いと、こんなに簡単にはいかないと思いますが、参考程度になれば幸いです。

ここまで読んでいたきありがとうございました。