タイピングゲームを作りながらTypeScriptを始めよう

# はじめに

本稿は[拓殖大学ディジタルコンテンツ研究愛好会 Advent Calendar 2020](https://adventar.org/calendars/5528) 2日目の記事です。
(このまま順調に卒業資格を得られればですが)大学生最後のアドベントカレンダーになります。

TypeScriptを実際に書き始めて、JavaScriptではなくTypeScriptを選ぶ気持ちと理由を理解することが出来ました。
そこで、後輩にも[TypeScript](https://www.typescriptlang.org/)を触ってほしいという気持ちを込めて、この場を借りて言語への入りとなるよう本稿を残します。

TypeScriptについては[TypeScript の概要](https://qiita.com/EBIHARA_kenji/items/4de2a1ee6e2a541246f6)を参考にしてほしいです。
よろしくお願いします。

# この記事の対象

- プログラミング言語の基礎を理解している
- DOMについて最低限の知識がある

上記にたり得ていない・自信がないという方は下記のページをお供に読んで頂ければと思います。

- [JavaScript Primer](https://jsprimer.net/)
- [HTML](https://developer.mozilla.org/ja/docs/Web/HTML)
- [CSS](https://developer.mozilla.org/ja/docs/Web/CSS)

# 環境

[stackblitz](https://stackblitz.com/)を使います。
sign inしたあと、下記画像のTypeScriptを選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/9727bc36-1d75-0c92-34e6-759dcf27c08c.png)
これでエディタと実行結果が表示されるページが表示されます。

# 完成目標

下記URIより遊べます。
https://typescript-oubejk.stackblitz.io

また、[github](https://github.com/masayasviel/typeGame)でコードを公開しています。
こちらは[Parcel](https://parceljs.org/getting_started.html)で動作を確認しました。

# HTML・CSS
最初から存在する`index.html`と`style.css`に書いていきます。
下記のコードをコピペします。

```index.html
<div id="main">
  <h1>タイピングゲーム</h1>
  <div id="frame"></div>
  <input type="button" id="restart" value="restart">
</div>
```

```style.css
#main {
  margin: 10px;
  padding: 0;
  border: 0;
}

#frame {
  width: 500px;
  height: 300px;
  padding: 0.5em;
  font-size: 24px;
  border: 5px double #000000;
  word-break: break-all;
  white-space: normal;
}

#restart {
  margin: 10px 20px;
  border-bottom: solid 3px #888;
  border-radius: 20px;
  width: 100px;
  height: 50px;
}
```

`index.ts`内にエラーが出ますが気にしないでください。

# TypeScript

TypeScriptを書いていきます。TypeScriptでは型推論が効きますが、最初はDOM周りの型で迷いがちなのであえて明記します。
本稿では先にコード全体を載せてから、その後で解説をツラツラと書き連ねていくスタイルを取ります。

## コード全体

```index.ts
import "./style.css";
import Question from "./question";

const qnum = 10;

function main() {
  const restart: HTMLElement = document.getElementById("restart");
  const frame: HTMLElement = document.getElementById("frame");

  if (!restart || !frame) return;

  const question: Question = new Question(qnum);
  frame.textContent = question.init();

  restart.addEventListener("click", () => {
    frame.textContent = question.init();
  });

  window.addEventListener("keydown", (e: KeyboardEvent) => {
    e.preventDefault();
    const res: string | boolean = question.main(e.key.toUpperCase());
    if (typeof res == "string") frame.textContent = res;
  });
}

main();
```

`question.ts`は存在しないので、新しいファイルを作ります。

```question.ts
const alphabet: string[] = [
    "A", "B", "C", "D", "E", "F", "G",
    "H", "I", "J", "K", "L", "M", "N",
    "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
];

export default class Questions {
  /** 問題数 */
  numOfQuestions: number;
  /** 問題文 */
  question: string;
  /** 現在が何問目か */
  selfNumber: number;

  /**
   * constructor
   * @param qnum 問題数
   */
  constructor(qnum: number) {
    this.numOfQuestions = qnum;
  }
  /**
   * 初期化する
   */
  public init(): string {
    this.question = "";
    this.selfNumber = 0;
    for (let i = 0; i < this.numOfQuestions; i++)
      this.question += alphabet[Math.round(Math.random() * 25)];
    return this.question;
  }

  /**
   * メイン処理
   * @param inputKey 入力されたアルファベット
   */
  public main(inputKey: string): string | boolean {
    if (this.question.charAt(0) === inputKey) {
      this.selfNumber++;
      if (this.selfNumber < this.numOfQuestions) {
        this.question = this.question.slice(1, this.question.length);
        return this.question;
      } else {
        return "終了します\nリセットボタンを押してください";
      }
    }
    return false;
  }
}
```

## question.ts

```typescript
const alphabet: string[] = [
    "A", "B", "C", "D", "E", "F", "G",
    "H", "I", "J", "K", "L", "M", "N",
    "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
];
```

string型の配列を定義しています。
TypeScriptでは型をコロンの後に書きます。また、配列は`Type[]`とします。

---

```typescript
export default
```

「そのファイルが`import`されるときになにを呼び出すか」を`export`によって定めます。
詳しくは[Typescript学習メモ①(ExportとImport、require("xxx")とmodule.exports)](https://qiita.com/zuzuwen/items/2577680a5432f5b15247)を見てください。

---

```typescript
  numOfQuestions: number;
  question: string;
  selfNumber: number;
```

class変数です。
`public`や`private`、`protected`といった修飾子を付けることも出来ます。
[TypeScript日本語ドキュメント](https://typescript-jp.gitbook.io/deep-dive/future-javascript/classes)をみると分かりやすいです。
余談ですが、getterとsetterは下記のように書けます。

```typescript
// getter
public get value(): T {
  return val;
}
// 使い方
const res = cls.value;

// setter
public set value(v: T) {
  this.val = v;
}
// 使い方
cls.value = v;
```

---

```typescript
for (let i = 0; i < this.numOfQuestions; i++)
      this.question += alphabet[Math.round(Math.random() * 25)];
```

問題数分ランダムにアルファベットを生成します。

---

```typescript
public main(inputKey: string): string | boolean {
  if (this.question.charAt(0) === inputKey) {
    this.selfNumber++;
    if (this.selfNumber < this.numOfQuestions) {
      this.question = this.question.slice(1, this.question.length);
      return this.question;
    } else {
      return "終了します\nリセットボタンを押してください";
    }
  }
  return false;
}
```

入力されたキーと一文字目を比べ、等しいかつ問題がまだ残っていれば一文字削除。
残っていなければ終わる旨を返します。
そもそも一文字目と入力が違うならば`false`を返します。
ここで注目なのが返り値が`boolean`と`string`で違う点です。

```typescript
public main(inputKey: string): string | boolean
```

から分かる通り、返り値に複数の型を宣言できます。
これは返り値に限らず変数や引数にも適応できます。

詳しくは[TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)を見てください。

```typescript
type T = string | boolean;
```

といったこともできます。

参考：[TypeScriptのInterfaceとTypeの比較](https://qiita.com/tkrkt/items/d01b96363e58a7df830e)

---

```typescript
/** コメント */
```

これはJSDocに乗っ取った書き方をしています。
このように書くことで、エディタ上で変数や関数にカーソルを合わせるとコメントで書いたそれについての説明が表示されるようになります。
関数であれば、

```typescript
/**
* 関数の説明
* @param 引数名 説明
* @return 返り値
*/
```

のような書き方ができ、外部から見てもその関数が何をする関数で何を受け取って何を返すのか型以上の情報を得ることができます。
JSDocで検索をかけてみると詳細な書き方が出てくるので、これを使って開発を楽に進めましょう。

## index.ts

```typescript
import "./style.css";
import Question from "./question";
```

ファイルをimportしています。cssファイルもimportできます。

---

```typescript
const restart: HTMLElement = document.getElementById("restart");
const frame: HTMLElement = document.getElementById("frame");
```

HTMLElement型というものがあります。
これが厄介でHTML○○Elementといったものがたくさんあります。
検索をかけたりエディタの機能を使ったりして特定し、怒られないよう詰めていきます。

また、idが見つからなかった場合エラーを起こしてしまう可能性があるので、

```typescript
if (!restart || !frame) return;
```

としておきます。

---

```typescript
const res: string | boolean = question.main(e.key.toUpperCase());
if (typeof res == "string") frame.textContent = res;
```

`question.main`にキー入力を大文字にしたものを渡します。
`e`は`KeyboardEvent`型ですが、`key`はstring型を返すため`toUpperCase()`が使えます。
大文字にするのは、`question.ts`から分かる通り、大文字で問題文を生成しているためです。
また、`question.main`はstringとbooleanを返すのに`frame.textContent = res`が出来るのは`if (typeof res == "string")`にて型を絞っているためです。
また、TypeScriptには[型アサーション](https://qiita.com/irico/items/9d71060e52ffc1e79962)という機能があります。
["!"(エクスクラメーション/感嘆符)](https://qiita.com/zigenin/items/364264a6cf635b962542)といったものもあります。

---

```typescript
main();
```

[Parcel](https://parceljs.org/getting_started.html)などを使うときは`window.addEventListener("load", main);`にしますが今回はこれでOK。

# おわりに

月並みではありますが、TypeScriptはJavaScriptとは違い、（DOM周りを書いていると型が煩わしくなるときがありますが）型があることやnull安全であることがやはり強力です。またコードを書いている段階でエディタ側が間違いを指摘してくれるのも大きな強みです。
是非これを機会にTypeScriptで遊んでもらえればと思います。
以上、[拓殖大学ディジタルコンテンツ研究愛好会 Advent Calendar 2020](https://adventar.org/calendars/5528) 2日目でした。